Using Knex.js Transactions to Guarantee All‑or‑Nothing Writes
Learn how Knex.js transactions automatically commit on success and roll back on error, preventing partial writes when inserting related rows such as orders and line items.
05 Aug 2025, 07:01 UTC

The problem: partial writes can corrupt data
When a request touches multiple tables – for example, creating an order and then inserting its line items – a failure after the first insert leaves the database in an inconsistent state. Manually checking each query and rolling back on error is tedious and error‑prone.
Thesis: Knex.js’ built‑in transaction API handles the commit/rollback for you
By wrapping related queries in knex.transaction and using the supplied transaction object for every query, Knex automatically starts a transaction, commits on success, and rolls back if any error bubbles out of the callback. This lets you focus on business logic instead of low‑level connection handling.
How the API works
The transaction function receives a callback that gets a trx object. All queries inside the callback must use trx; using the main knex instance bypasses the transaction and can cause partial commits. If the callback throws or returns a rejected promise, Knex issues a ROLLBACK. If it resolves successfully, Knex issues a COMMIT. Returning a value from the callback resolves the outer promise with that value, making it easy to return generated IDs or aggregates.
Worked example: creating an order with line items
// Assume you have a Knex instance configured as `knex`
async function createOrder(customerId, items) {
return knex.transaction(async trx => {
// 1️⃣ Insert the order header
const [orderId] = await trx('orders')
.insert({ customer_id: customerId, created_at: new Date()})
.returning('id');
// 2️⃣ Prepare line‑item rows
const lineItems = items.map(item => ({
order_id: orderId,
sku: item.sku,
quantity: item.qty,
price: item.price
}));
// 3️⃣ Insert line items
await trx('order_items').insert(lineItems);
// 4️⃣ Return the new order ID for the caller
return orderId;
});
}
// Usage
createOrder(42, [
{ sku: 'ABC-1', qty: 2, price: 12.5 },
{ sku: 'XYZ-9', qty: 1, price: 8.0 }
])
.then(id => console.log(`Order ${id} created`))
.catch(err => console.error('Transaction failed:', err));
If any of the inserts fails – say, a foreign‑key violation on order_items – the callback throws, Knex rolls back both the order header and any line items that may have been inserted, leaving the database unchanged.
Trade‑offs and limitations
- Explicit use of the transaction object – Forgetting to use
trxfor a query means that query runs outside the transaction and can commit even when the rest fails. - No nested transactions by default – Starting a second
knex.transactioninside the first creates a separate connection; it does not share the same isolation scope. If you need true nesting, you must use savepoints or a library that supports them. - Error handling outside the callback – If you catch an error inside the callback and swallow it, the transaction will still commit because Knex sees no rejection. Let errors propagate or explicitly call
trx.rollback()when you decide to abort.
Actionable closing
Next time you need to perform multiple related writes, wrap them in a knex.transaction callback, use the supplied trx for every query, and let Knex handle the commit or rollback. Verify the behavior in a development environment by enabling the debug flag (knex({ debug: true })) and checking that the console shows BEGIN, the relevant INSERT statements, and either COMMIT or ROLLBACK depending on whether an error is thrown.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.