Fix Knex Transaction Rollback – Transaction Not Rolling Back
In this tutorial, you'll learn about Fix Knex Transaction Rollback. We cover key concepts, practical examples, and best practices.
You call trx.rollback() in a Knex transaction, but the changes are still persisted in the database. Or the transaction never completes — the query hangs until the timeout.
Wrong ❌
// ❌ Improper transaction handling
const trx = await knex.transaction();
try {
await trx('users').insert({ name: 'Alice' });
await trx('posts').insert({ title: 'Post', userId: 999 }); // FK violation!
// The error is caught, but what about the transaction?
} catch (err) {
console.error(err);
// The transaction is left open — no rollback!
}
The first INSERT (users) is already sent. The second fails. The transaction is left open, holding a connection until timeout.
Right ✅
// ✅ Managed transaction (auto rollback on error)
await knex.transaction(async (trx) => {
const [userId] = await trx('users').insert({ name: 'Alice' }).returning('id');
// If this fails, the transaction is automatically rolled back
await trx('posts').insert({ title: 'Post', userId: 999 });
});
Manual transaction with proper rollback:
const trx = await knex.transaction();
try {
await trx('users').insert({ name: 'Alice' });
await trx('posts').insert({ title: 'Post', userId: 1 });
await trx.commit();
console.log('Transaction committed');
} catch (err) {
await trx.rollback(); // ✅ revert all changes
console.error('Transaction rolled back:', err.message);
}
If rollback() doesn't roll back — check isolation:
PostgreSQL automatically rolls back a transaction when a statement error occurs. If you catch the error and call rollback(), it may already be rolled back:
try {
await trx('posts').insert({ invalidColumn: 'value' }); // SQL error
await trx.commit();
} catch (err) {
// The transaction is already aborted by PostgreSQL
await trx.rollback(); // This may throw "no such savepoint"
// Use a fresh connection or re-throw
}
Use the managed pattern to avoid this:
// ✅ Safe — handles all edge cases
await knex.transaction(async (trx) => {
await trx('posts').insert({ id: 1, title: 'Test' });
// Any thrown error triggers rollback automatically
});
Transaction with savepoints:
await knex.transaction(async (trx) => {
await trx('users').insert({ id: 1, name: 'Alice' });
try {
await trx.transaction(async (subTrx) => {
await subTrx('posts').insert({ id: 1, title: 'Partial' });
throw new Error('Rollback sub-transaction only');
});
} catch (err) {
// Only the inner transaction is rolled back (savepoint)
}
// The outer transaction continues
await trx('users').insert({ id: 2, name: 'Bob' });
});
// Both Alice and Bob are saved; the post is not
Debugging hung transactions:
SELECT pid, state, query, state_change
FROM pg_stat_activity
WHERE state = 'idle in transaction';
-- Kill: SELECT pg_terminate_backend(pid);
Root Cause
Knex transactions use PostgreSQL's BEGIN/COMMIT/ROLLBACK. If rollback() is called after the connection already aborted the transaction (due to a SQL error), the call fails silently or throws. Using the managed knex.transaction(async trx => ...) pattern handles all cases correctly.
Prevention
- Always use the managed
knex.transaction(async (trx) => {...})pattern. - Never catch errors without rolling back in a manual transaction.
- Use savepoints for partial rollbacks.
- Monitor
pg_stat_activityfor idle-in-transaction connections.
Common Mistakes with transaction rollback
- Forgetting that lazy evaluation defers computation until the value is forced, causing space leaks with unevaluated thunks
- Using
returnto exit a function early instead of wrapping a pure value in the monad - Mixing let bindings with <- bindings in do notation, producing type errors
These mistakes appear frequently in real-world KNEX code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.
Practice Exercise
Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.
This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.
FAQ
Transactions are covered in the DodaTech Knex.js Advanced Patterns course.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro