EditTxn: Explicit Editing Transactions
Fast migration path
If you already know the existing write APIs and just need to keep shipping, do this first:
- Wrap each write workflow in withEditTxn.
- Replace deprecated write calls with txn-first overloads.
- Use
txn.saveChanges(...)only when you want an intermediate commit and keep writing in the same transaction scope. - If you are using direct
EditTxn(not withEditTxn), usetxn.end("save")to finish, ortxn.end("abandon")to discard pending edits. - In dependency callbacks, use the callback argument's
indirectEditTxninstead of creating a new transaction.
Old API to new API mapping
| Existing pattern | EditTxn pattern |
|---|---|
element.insert() |
element.insert(txn) |
element.update() |
element.update(txn) |
element.delete() |
element.delete(txn) |
iModel.saveChanges("desc") |
txn.saveChanges("desc") |
iModel.abandonChanges() |
txn.abandonChanges() |
Model.insert(...) |
Model.insert(txn, ...) |
Relationship.insert(...) |
Relationship.insert(txn, ...) |
When possible, start with withEditTxn and migrate call sites one workflow at a time.
Background
SQLite executes reads and writes within transactions. A read transaction sees a stable view of the database until that read transaction ends, so commits made by other connections are not visible until the next transaction starts.
iTwin.js builds on top of that behavior. Each IModelDb has a native-managed implicit transaction that keeps query behavior consistent even when another connection changes the file.
Historically, legacy write APIs also used that implicit transaction. That made writing convenient, but it could blur transaction boundaries: unrelated edits could accumulate into one unit of work and then be saved or undone together.
Why EditTxn
EditTxn introduces explicit transaction boundaries so callers can define a deliberate unit of work:
- Start editing when you intend to begin the unit of work.
- Make one or more changes through that transaction.
- Save or abandon that exact scope.
This improves clarity and reduces accidental coupling between unrelated edits.
It also makes undo behavior more predictable. Without explicit boundaries, unrelated edits can be combined into the same implicit unit of work, and a later undo can reverse those combined changes unexpectedly.
Migration model
Migration is incremental:
- Legacy implicit-write APIs remain available during the transition and are deprecated in favor of explicit APIs.
- New write paths should use explicit EditTxn APIs.
- Existing code can migrate call sites gradually to txn-first overloads or withEditTxn.
The target end state is explicit write transactions for all writes, with the implicit transaction used only for read behavior.
Temporary deprecation-lint containment
If this change introduces too many deprecation lint errors at once, you can temporarily silence specific call sites while you keep shipping.
Prefer narrow suppression on individual lines and always add a TODO marker you can search for later.
For short migration windows, you can suppress a small block, but keep the TODO scoped and explicit.
Recommended follow-up:
- Track these TODOs in a migration issue or backlog item.
- Search for
TODO(EditTxn-migration)before release and remove suppressions as call sites are migrated.
Common failure modes
- Transaction is not active: start the transaction (
txn.start()) before writing, or use withEditTxn. - Another transaction is active: only one explicit transaction can be active per iModel at a time.
- Unsaved changes exist before
start(): in practice this usually means legacy implicit-write APIs have already produced pending changes on the iModel; save or abandon those changes before starting a new explicit transaction. - In indirect dependency callbacks, a new transaction is created instead of reusing the callback transaction: use the callback argument's
indirectEditTxn.
implicitWriteEnforcement
EditTxn.implicitWriteEnforcement, initialized from IModelHostOptions.implicitWriteEnforcement, controls how legacy implicit writes behave while you migrate:
allow: keep implicit writes working.log: allow implicit writes but logimplicit-txn-write-disallowederrors to help identify remaining migration work.throw: reject implicit writes and require explicit EditTxn usage.
log can be noisy in applications that have not started migration, because each implicit write path emits an error log.
Indirect change callbacks
During indirect dependency processing callbacks (for example relationship callbacks), use the callback argument's indirectEditTxn to access the active transaction for that scope.
Examples
Recommended scoped pattern with withEditTxn
This is the preferred migration pattern for most existing write workflows.
More complete withEditTxn flow
Assume parentSubjectId, category, and importedRows are already resolved by your workflow.
Direct EditTxn usage
How EditCommand uses EditTxn
In editing workflows, backend EditCommand uses EditTxn as its write surface.
- Each command instance creates its own
EditTxn. beginEditing()starts the command transaction.- Command writes are expected to use that transaction (
this.txn) so edits stay grouped by command source. saveChanges()on the command commits pending edits but keeps the command transaction active.endEdits()saves and ends the transaction;abandonEdits()abandons and ends it.
This pattern helps keep an editing session coherent by ensuring one active command owns one active transaction scope at a time.
EditCommand migration checklist
- Call
beginEditing()before the first write. - Route all writes through
this.txn. - Use
saveChanges()for intermediate checkpoints when needed. - Call
endEdits()when work completes successfully. - Call
abandonEdits()when work is cancelled or invalid.
Related APIs
- EditTxn
- withEditTxn
- OnDependencyArg
- OnElementDependencyArg
- IModelHostOptions.implicitWriteEnforcement
- EditCommand
References
Last Updated: 08 April, 2026