drizzle-team/drizzle-orm · warning · TransactionRollbackError

Rollback

Error message

Rollback

What it means

Thrown by GelTransaction.rollback() (line 231) as a TransactionRollbackError. This is the intentional control-flow mechanism Drizzle uses to abort a transaction: calling tx.rollback() throws, which propagates out of the transaction callback and signals the session wrapper to issue a ROLLBACK instead of COMMIT.

Source

Thrown at drizzle-orm/src/gel-core/session.ts:231

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

	constructor(
		dialect: GelDialect,
		session: GelSession<any, any, any>,
		protected schema: {
			fullSchema: Record<string, unknown>;
			schema: TSchema;
			tableNamesMap: Record<string, string>;
		} | undefined,
	) {
		super(dialect, session, schema);
	}

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

	abstract override transaction<T>(
		transaction: (tx: GelTransaction<TQueryResult, TFullSchema, TSchema>) => Promise<T>,
	): Promise<T>;
}

export interface GelQueryResultHKT {
	readonly $brand: 'GelQueryResultHKT';
	readonly row: unknown;
	readonly type: unknown;
}

export type GelQueryResultKind<TKind extends GelQueryResultHKT, TRow> = (TKind & {
	readonly row: TRow;
})['type'];

View on GitHub (pinned to b7862528fd)

Solutions

  1. Recognize this is expected behavior: do NOT catch and ignore TransactionRollbackError unless you intend to suppress the rollback signal.
  2. If you catch errors around db.transaction, re-throw TransactionRollbackError so the session still rolls back.
  3. Use the return value of db.transaction for success and let rollback() handle abort - don't structure code around the throw.
  4. If you see this leak to a caller unexpectedly, ensure the transaction callback isn't wrapped in a try/catch that swallows it.

Example fix

// before - swallowing rollback breaks the signal
try {
  await db.transaction(async (tx) => {
    if (!ok) await tx.rollback();
  });
} catch (e) { /* swallowed */ }

// after - let it propagate or re-throw
import { TransactionRollbackError } from 'drizzle-orm/errors';
try {
  await db.transaction(async (tx) => {
    if (!ok) tx.rollback();
  });
} catch (e) {
  if (e instanceof TransactionRollbackError) return; // expected
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Decide rollback before entering the transaction
if (!shouldCommit) return null;
return await db.transaction(async (tx) => { ... });

Type guard

import { TransactionRollbackError } from '~/errors.ts';
function isRollback(e: unknown): e is TransactionRollbackError {
  return e instanceof TransactionRollbackError;
}

Try / catch

try {
  await db.transaction(async (tx) => {
    if (!ok) tx.rollback();
    // ...
  });
} catch (e) {
  if (e instanceof TransactionRollbackError) return; // expected
  throw e;
}

Prevention

When it happens

Trigger: User explicitly calls tx.rollback() inside a db.transaction(async (tx) => {...}) callback. Any code path that reaches rollback() will throw; the Gel session catches it and rolls back the underlying transaction.

Common situations: Business-rule validation inside a transaction decides the operation should not commit; a nested transaction (savepoint) needs to be aborted; wrapping rollback in custom error handling that accidentally swallows it.

Related errors


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