drizzle-team/drizzle-orm · warning · TransactionRollbackError

Rollback

Error message

Rollback

What it means

PgTransaction.rollback (session.ts:256) intentionally throws a TransactionRollbackError (a DrizzleError subclass named 'TransactionRollbackError' with message 'Rollback'). It is a control-flow signal: Drizzle's transaction wrapper catches it to issue ROLLBACK and abort the callback cleanly. It is not a bug — it is the supported way to abort a transaction from inside.

Source

Thrown at drizzle-orm/src/pg-core/session.ts:257

	TSchema extends TablesRelationalConfig = Record<string, never>,
> extends PgDatabase<TQueryResult, TFullSchema, TSchema> {
	static override readonly [entityKind]: string = 'PgTransaction';

	constructor(
		dialect: PgDialect,
		session: PgSession<any, any, any>,
		protected schema: {
			fullSchema: Record<string, unknown>;
			schema: TSchema;
			tableNamesMap: Record<string, string>;
		} | undefined,
		protected readonly nestedIndex = 0,
	) {
		super(dialect, session, schema);
	}

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

	/** @internal */
	getTransactionConfigSQL(config: PgTransactionConfig): SQL {
		const chunks: string[] = [];
		if (config.isolationLevel) {
			chunks.push(`isolation level ${config.isolationLevel}`);
		}
		if (config.accessMode) {
			chunks.push(config.accessMode);
		}
		if (typeof config.deferrable === 'boolean') {
			chunks.push(config.deferrable ? 'deferrable' : 'not deferrable');
		}
		return sql.raw(chunks.join(' '));
	}

	setTransaction(config: PgTransactionConfig): Promise<void> {

View on GitHub (pinned to b7862528fd)

Solutions

  1. Let TransactionRollbackError propagate out of the callback so Drizzle's wrapper handles ROLLBACK.
  2. At the db.transaction() call site, catch TransactionRollbackError to treat it as an expected abort rather than a crash.
  3. Do not wrap tx.rollback() in a try/catch inside the callback that swallows it.

Example fix

// before
try {
  await db.transaction(async (tx) => {
    if (!ok) await tx.rollback();
  });
} catch (e) { /* treated as crash */ }

// after
try {
  await db.transaction(async (tx) => {
    if (!ok) tx.rollback();
  });
} catch (e) {
  if (e instanceof TransactionRollbackError) {/* expected abort */}
  else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Centralize transaction abort handling at the call site.
async function runTx<T>(db: any, fn: (tx: any) => Promise<T>): Promise<T | null> {
  try {
    return await db.transaction(fn);
  } catch (e) {
    if (e instanceof TransactionRollbackError) return null; // expected abort
    throw e;
  }
}

Type guard

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

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

Try / catch

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

try {
  await db.transaction(async (tx) => {
    if (!shouldCommit) tx.rollback();
    await tx.insert(t).values(row);
  });
} catch (e) {
  if (e instanceof TransactionRollbackError) {
    // expected abort; nothing was committed
  } else {
    throw e; // real failure
  }
}

Prevention

When it happens

Trigger: Calling tx.rollback() inside a db.transaction(async (tx) => { ... }) callback when business logic decides the work should not commit. The thrown error propagates unless caught by the wrapper.

Common situations: Validation failure inside a transaction; optimistic concurrency conflict; partial work that must be discarded; wrapping rollback in a try/catch that accidentally swallows the signal.

Related errors


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