Mastering Sequelize Transactions: Start, Commit, Rollback, and Nested Savepoints
Sequelize transactions let you bundle database operations into an atomic unit. Learn how to start, commit, rollback, and use nested transactions with async/await, including pitfalls and best‑practice patterns.
20 Jul 2025, 18:44 UTC

Why Transactions Matter in Sequelize
When you’re writing data‑intensive Node.js applications, a single bug can corrupt an entire dataset. Sequelize’s transaction API lets you bundle a series of operations into an atomic unit: either all succeed or none do. The key is to understand how to start a transaction, pass it to every query, and cleanly finish it.
Getting Started – The Core Pattern
Sequelize exposes two ways to create a transaction:
1. Callback form – automatically rolls back on error.
2. Promise/async‑await form – gives you explicit control.
Below is the most common async/await pattern that works in Sequelize v6+.
// Import the Sequelize instance
const { sequelize, User } = require('./models');
async function createUserWithProfile() {
const t = await sequelize.transaction(); // start a new transaction
try {
const user = await User.create({ name: 'Alice' }, { transaction: t });
// ... other operations that must belong to the same transaction
await User.update({ active: true }, { where: { id: user.id }, transaction: t });
await t.commit(); // persist all changes
return user;
} catch (err) {
await t.rollback(); // undo everything on error
throw err;
}
}
Key points:
- The
transactionobject is passed to every Sequelize call that should be part of the unit. - Both
commit()androllback()return a Promise; await them to guarantee completion. - Sequelize sets
t.finishedto"commit"or"rollback"after resolution – useful for debugging.
Callback Form – Automatic Rollback
If you prefer less boilerplate, wrap the logic in the callback form. Sequelize will automatically roll back if the callback throws or rejects.
sequelize.transaction(async (t) => {
await User.create({ name: 'Bob' }, { transaction: t });
// any thrown error will trigger rollback
});
Even though you don’t call commit() or rollback() manually, you still need to handle errors in the surrounding code to react accordingly.
Nested Transactions – Savepoints in Action
Sequelize supports nested transactions via transaction.transaction(). Internally, these are savepoints – a lightweight mechanism that lets you roll back part of a transaction without aborting the whole.
async function complexOperation() {
const outer = await sequelize.transaction();
try {
await User.create({ name: 'Outer' }, { transaction: outer });
const inner = await outer.transaction(); // create a savepoint
try {
await User.create({ name: 'Inner' }, { transaction: inner });
await inner.commit(); // release savepoint
} catch (e) {
await inner.rollback(); // revert only inner changes
throw e; // bubble up to outer
}
await outer.commit(); // commit everything
} catch (err) {
await outer.rollback();
console.error('Operation failed', err);
}
}
Result:
- If
innersucceeds butouterfails, bothInnerandOuterrows are removed. - If
innerfails butoutersucceeds,Inneris rolled back whileOuterremains.
Common Pitfalls and How to Avoid Them
- Forgetting the
{ transaction }option
Raw SQL or Sequelize methods that don’t receive the transaction object run outside the transaction. Always pass{ transaction: t }. - Sharing a transaction across concurrent async calls
Each logical unit of work should own its own transaction. Reusing a transaction object in parallel functions can lead to unpredictable lock states. - Leaving a transaction unresolved
If you never callcommit()orrollback(), the connection stays busy and locks may be held until the pool releases the connection. Always guard withtry/catch/finallyor the callback form. - SQLite in‑memory mode
Transactions are per connection. If you spin up multiple Sequelize instances against an in‑memory SQLite database, each gets its own connection and transactions won’t share state. Use a file‑based SQLite or a single instance. - Isolation level mis‑configuration
The default isREAD COMMITTED. For stricter guarantees (e.g.,SERIALIZABLE), explicitly set it:await sequelize.transaction({ isolationLevel: sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE }, async (t) => { /* … */ });
Practical Verification Checklist
| Check | How |
|---|---|
| Transaction committed? | Query the table after commit() and confirm changes exist. |
| Transaction rolled back? | Query after rollback(); data should be absent. |
| Nested transaction behavior? | Commit inner, rollback outer; both sets of changes should be undone. |
| Isolation level? | Start two concurrent transactions with SERIALIZABLE and attempt conflicting writes; the second should wait. |
| Raw query inside transaction? | Run a raw INSERT without { transaction }; after commit, verify that the row is not present. |
When to Use Nested Transactions
Use nested transactions when you want to isolate a subset of operations that might fail independently. For example, updating user data while also attempting to send a notification email: if the email fails, you may want to roll back the notification insert but keep the user update.
Final Takeaway
Sequelize’s transaction API is straightforward once you adhere to the pattern: start a transaction, pass it to every query, and always finish with commit() or rollback(). Nested transactions give you granular control, but require careful isolation of transaction objects. By following the checklist above and avoiding the common pitfalls, you’ll keep your data consistent and your application robust.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.