Skip to content

Fix Knex Transaction Rollback – Transaction Not Rolling Back

DodaTech Updated 2026-06-24 3 min read

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.

// ✅ 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_activity for idle-in-transaction connections.

Common Mistakes with transaction rollback

  1. Forgetting that lazy evaluation defers computation until the value is forced, causing space leaks with unevaluated thunks
  2. Using return to exit a function early instead of wrapping a pure value in the monad
  3. 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

**Q: Can I use transactions with raw queries?**

A: Yes — pass { connection: trx } to knex.raw().

**Q: What happens if I don't call `commit` or `rollback`?**

A: The transaction stays open until the connection is destroyed or times out.

**Q: Does Knex support distributed transactions?**

A: No — Knex works with a single database. For distributed transactions, use a saga pattern.

**Q: Can I use `async/await` with transactions?**

A: Yes — all Knex transaction methods support promises.


Transactions are covered in the DodaTech Knex.js Advanced Patterns course.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro