Diagnosing Knex.js Transaction Rollback Failures in Async Code
When an error occurs inside an async transaction in Knex.js, the database may commit before the error is handled, leaving data inconsistent. This guide shows how to spot the problem, identify root causes, and apply fixes that guarantee rollback.
18 Aug 2025, 11:07 UTC

Recognizing the Symptom
After calling trx.commit() in a Knex transaction, your application logs an error but the database still contains the changes. The error originates from an await inside a nested async function that was never awaited by the outer transaction scope.
Typical symptoms:
- Transaction logs show
COMMITbefore the error is thrown. - Database rows remain inserted or updated even though the application should have rolled back.
- Stack traces reference async callbacks that are not wrapped in
try/catch.
Common Causes
| Cause | Symptom | Diagnostic Check | Fix |
|---|---|---|---|
| Async function outside transaction scope | Commit occurs before error propagates | Check if nested async functions are awaited | Await all async calls or pass trx explicitly |
Missing try/catch around transaction logic | Unhandled promise rejection, no rollback | Search for trx.commit() without surrounding try | Wrap transaction block in try/catch and call trx.rollback() in catch |
| Using raw driver calls that bypass Knex transaction context | Connection not released, orphaned transaction | Look for connection.query() or similar | Use trx.raw() or pass trx to driver methods |
| Auto‑commit mode enabled on the driver | Commit happens automatically, ignoring Knex boundaries | Check driver configuration for autocommit | Disable auto‑commit or use Knex's transaction wrapper |
| Isolation level mismatch causing early commit | Transaction commits before error is seen | Verify isolation level in trx options | Set appropriate level (e.g., READ COMMITTED) |
Diagnostic Checklist
- Enable Knex debug mode. Add
debug: trueto the client config to view SQL logs.const knex = require('knex')({ client: 'pg', connection: process.env.PG_URI, debug: true }); - Run the failing transaction in isolation. Create a test migration that intentionally throws inside a transaction.
exports.up = async function(knex) { await knex.transaction(async trx => { await trx('users').insert({name: 'temp'}); throw new Error('Intentional failure'); }); }; - Check the logs for
ROLLBACKstatements. If absent, the transaction was not rolled back. - Verify that all async calls are awaited. Unawaited promises will resolve after the transaction commits.
- Inspect any raw driver usage. Ensure it receives
trxas its connection.
Fixing the Code
Below is a minimal, correct pattern that guarantees rollback on error.
async function safeInsert(knex, userData) {
try {
await knex.transaction(async trx => {
// All statements use the trx object
await trx('users').insert(userData);
// If any async operation fails, the error propagates
await someAsyncCheck(userData.email);
});
} catch (err) {
// Transaction is automatically rolled back by Knex
console.error('Insert failed, transaction rolled back:', err);
throw err;
}
}
Key points:
- All database calls inside the transaction use the
trxobject. - The
try/catchsurrounds the entire transaction call, not just the inner logic. - Knex automatically performs
trx.rollback()if the transaction callback throws. - Any async function called inside the transaction must be awaited.
Verification Steps
- Run the test migration:
npx knex migrate:latest --env test - Confirm that the
userstable remains unchanged after the migration failure. - Look at the console output; you should see a
ROLLBACKstatement followed by the error message. - Optionally, enable
knex.on('query-response', ...)to programmatically assert that no rows were inserted.
Escalation Criteria
If after applying the fixes:
- The database still shows committed changes after an error.
- Logs do not contain a
ROLLBACKentry. - You cannot reproduce the issue in a local environment.
Consider the following steps:
- Check the database driver version and its
autocommitsetting. - Verify that the Knex version is at least 0.21, which introduced better async support.
- Consult the driver’s documentation for transaction handling nuances.
- If the problem persists, open an issue on the Knex GitHub repository with a minimal reproduction.
Following this diagnostic flow ensures that your Knex.js transactions behave predictably, even when complex async logic is involved.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.