drizzle-team/drizzle-orm · warning · TransactionRollbackError

Rollback

Error message

Rollback

What it means

A TransactionRollbackError (subclass of DrizzleError) with message 'Rollback', thrown by MySqlTransaction.rollback() (drizzle-orm/src/mysql-core/session.ts:259). Calling tx.rollback() inside a db.transaction() callback is the documented way to abort; the thrown error unwinds the callback so the driver can issue ROLLBACK. It is expected control flow, not a defect, when the developer intentionally aborts.

Source

Thrown at drizzle-orm/src/mysql-core/session.ts:259

	TQueryResult extends MySqlQueryResultHKT,
	TPreparedQueryHKT extends PreparedQueryHKTBase,
	TFullSchema extends Record<string, unknown> = Record<string, never>,
	TSchema extends TablesRelationalConfig = Record<string, never>,
> extends MySqlDatabase<TQueryResult, TPreparedQueryHKT, TFullSchema, TSchema> {
	static override readonly [entityKind]: string = 'MySqlTransaction';

	constructor(
		dialect: MySqlDialect,
		session: MySqlSession,
		protected schema: RelationalSchemaConfig<TSchema> | undefined,
		protected readonly nestedIndex: number,
		mode: Mode,
	) {
		super(dialect, session, schema, mode);
	}

	rollback(): never {
		throw new TransactionRollbackError();
	}

	/** Nested transactions (aka savepoints) only work with InnoDB engine. */
	abstract override transaction<T>(
		transaction: (tx: MySqlTransaction<TQueryResult, TPreparedQueryHKT, TFullSchema, TSchema>) => Promise<T>,
	): Promise<T>;
}

export interface PreparedQueryHKTBase extends MySqlPreparedQueryHKT {
	type: MySqlPreparedQuery<Assume<this['config'], MySqlPreparedQueryConfig>>;
}

View on GitHub (pinned to b7862528fd)

Solutions

  1. Wrap db.transaction(...) in try/catch and check for TransactionRollbackError specifically; treat it as 'aborted by app', not a failure.
  2. Ensure your rollback is genuinely intentional; if it fired unexpectedly, find the code path that called tx.rollback().
  3. Do not swallow all errors — only swallow TransactionRollbackError when the abort was deliberate.

Example fix

// before
await db.transaction(async (tx) => {
  await tx.insert(users).values(payload);
  if (!valid) tx.rollback(); // throws TransactionRollbackError, unhandled
});

// after
import { TransactionRollbackError } from 'drizzle-orm/errors';
try {
  await db.transaction(async (tx) => {
    await tx.insert(users).values(payload);
    if (!valid) tx.rollback();
  });
} catch (e) {
  if (e instanceof TransactionRollbackError) return { ok: false, reason: 'validation' };
  throw e;
}
Defensive patterns

Strategy: try-catch

Type guard

import { TransactionRollbackError } from 'drizzle-orm/errors';

function isIntentionalRollback(e: unknown): e is TransactionRollbackError {
  return e instanceof TransactionRollbackError;
}

Try / catch

try {
  await db.transaction(async (tx) => {
    await tx.insert(users).values(payload);
    if (!valid) tx.rollback();
  });
} catch (e) {
  if (e instanceof TransactionRollbackError) {
    return { ok: false, reason: 'business-rule validation aborted tx' };
  }
  throw e; // a real error
}

Prevention

When it happens

Trigger: Inside db.transaction(async tx => { ...; tx.rollback(); }), the explicit rollback() call throws this error to short-circuit the callback. Any caller that does not wrap the transaction in try/catch will see it propagate.

Common situations: A developer adds business-rule validation inside a transaction and calls tx.rollback() to cancel, but forgets that rollback() throws, so the error bubbles to an outer handler that treats it as a crash. Also common when porting code from a driver whose rollback does not throw.

Related errors


AI-assisted analysis of drizzle-team/drizzle-orm@b7862528fd (2026-08-03). Data as JSON: /data/errors/f1686204450fd939.json. Report an issue: GitHub.