Knex.js Transactions: Thread the Trx Object Correctly to Avoid Deadlocks
In Knex.js, a transaction only works if all queries inside the callback use the passed trx object. Mixing knex and trx, forgetting to await, or running long transactions can cause deadlocks and silent failures. This guide shows correct usage, common pitfalls, and verification steps.
20 May 2026, 23:40 UTC

Why Threading the Trx Object Matters
When you wrap a series of SQL statements in knex.transaction(async (trx) => { … }), you expect all of those statements to run under the same database connection and be committed or rolled back together. In practice this only happens if every query inside the callback is built from the trx instance that the callback receives. Using the global knex instance instead of trx creates a new connection, bypasses the transaction, and can silently corrupt data or exhaust the connection pool.
Correct Usage – A Worked Example
Below is a minimal, production‑ready snippet that demonstrates a safe transaction in a Node.js application using Knex with PostgreSQL. Replace <YOUR_DB_CONFIG> with your actual connection settings.
const knex = require('knex')({
client: 'pg',
connection: <YOUR_DB_CONFIG>,
pool: { min: 2, max: 10 }
});
async function createUserWithProfile(userData, profileData) {
// The callback style automatically commits on resolve
// and rolls back on any thrown error.
await knex.transaction(async (trx) => {
// All queries must use trx.
const [userId] = await trx('users')
.insert(userData)
.returning('id');
// Profile is linked to the newly created user.
await trx('profiles')
.insert({ ...profileData, user_id: userId });
// Intentionally throw to test rollback.
// throw new Error('Simulated failure');
});
}
Key points in the example:
- All queries use
trx:trx('users')andtrx('profiles').- Await every query: The transaction will not commit until the promise chain resolves.
- Throw to trigger rollback: Any uncaught exception inside the callback causes Knex to roll back automatically.
Common Pitfalls and How to Detect Them
1. Mixing knex and trx Inside the Callback
Using knex('users') instead of trx('users') will execute on a new connection. The transaction will commit or roll back, but that particular query is outside the transaction scope.
Detection
Enable Knex debug logging (debug: true) and look for the connection id in the log output. All statements inside a transaction should share the same id.
2. Forgetting to await a Query
If you write trx('users').insert(...); without await, the transaction may commit before the insert finishes. Errors from that query are lost.
3. Long‑Running Transactions Exhaust the Pool
Every open transaction holds a connection. Under high concurrency, a transaction that takes seconds can block the pool, leading to timeouts or deadlocks. Keep transactions short and avoid heavy CPU work inside them.
Limitations & Compatibility Notes
- Isolation Levels: Knex exposes
trx.isolationLevel('READ COMMITTED'), but the underlying database must support the requested level. SQLite ignores isolation level changes because it serializes writes. - Savepoints:
trx.savepoint()is only supported on dialects that implement it (PostgreSQL, MySQL 5.5+, MariaDB). Verify in your version’s docs. - Version Differences: The API surface (e.g.,
trx.rollback()vs. throwing) varies between Knex 0.95 and later releases. Always check theknexfile.jsyou ship with your application.
Practical Verification Steps
- Write a unit test: Insert a row inside a transaction, throw an error, and assert the row is absent afterward.
- Run with
debug: true: Confirm every statement inside the transaction shares the same connection id. - Observe
pg_stat_activity(PostgreSQL) orSHOW PROCESSLIST(MySQL): Verify that a transaction holds a single connection and that no stray queries leak outside the transaction.
When to Use Manual Commit/Rollback
The callback style is the safest and simplest. Manual control (e.g., const trx = await knex.transaction(); … await trx.commit();) can be useful when you need to pass the transaction object across async boundaries, but you must remember to await trx.commit() or await trx.rollback() to release the connection. A missed rollback leaks a pooled connection until it times out.
Takeaway
Always build queries with the trx object you receive in the transaction callback, await each query, and avoid mixing knex and trx inside the same block. This guarantees that your work is truly transactional, prevents deadlocks, and keeps your connection pool healthy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.