drizzle-team/drizzle-orm · info · Error

Nested transactions are handled by NetlifyDbTransaction via

Error message

Nested transactions are handled by NetlifyDbTransaction via savepoints

What it means

An Error 'Nested transactions are handled by NetlifyDbTransaction via savepoints' thrown by NetlifyDbWsSession.transaction() (drizzle-orm/src/netlify-db/session.ts:263). This is an internal guard: NetlifyDbSession.transaction() opens a real transaction using an internal WebSocket session (NetlifyDbWsSession), and nested transactions inside it must go through NetlifyDbTransaction.transaction() (which emits SAVEPOINTs). If something calls the WsSession's transaction() directly, it throws to redirect to the savepoint path.

Source

Thrown at drizzle-orm/src/netlify-db/session.ts:263

			rowMode: 'array',
			text: query,
			values: params,
		});
		return result;
	}

	async queryObjects<T extends QueryResultRow>(
		query: string,
		params: unknown[],
	): Promise<QueryResult<T>> {
		return this.client.query<T>(query, params);
	}

	override async transaction<T>(
		_transaction: (tx: NetlifyDbTransaction<TFullSchema, TSchema>) => Promise<T>,
		_config?: PgTransactionConfig,
	): Promise<T> {
		throw new Error('Nested transactions are handled by NetlifyDbTransaction via savepoints');
	}
}

export class NetlifyDbTransaction<
	TFullSchema extends Record<string, unknown>,
	TSchema extends V1.TablesRelationalConfig,
> extends PgTransaction<NeonHttpQueryResultHKT, TFullSchema, TSchema> {
	static override readonly [entityKind]: string = 'NetlifyDbTransaction';

	override async transaction<T>(
		transaction: (tx: NetlifyDbTransaction<TFullSchema, TSchema>) => Promise<T>,
	): Promise<T> {
		const savepointName = `sp${this.nestedIndex + 1}`;
		const tx = new NetlifyDbTransaction<TFullSchema, TSchema>(
			this.dialect,
			this.session,
			this.schema,
			this.nestedIndex + 1,

View on GitHub (pinned to b7862528fd)

Solutions

  1. Call tx.transaction(...) on the NetlifyDbTransaction passed to your callback, never on db.session.
  2. Do not reach into internal session objects; use the public db.transaction() / tx.transaction() API.
  3. If it surfaces from library code, report it — the public API should route nesting through NetlifyDbTransaction.

Example fix

// before (misuse of internals)
await (db as any).session.transaction(async (sp) => { /* ... */ }); // throws — wrong entry point

// after — nest via the handed-back transaction object (savepoints)
await db.transaction(async (tx) => {
  await tx.transaction(async (sp) => { /* SAVEPOINT sp1 */ });
});
Defensive patterns

Strategy: validation

Validate before calling

import { NetlifyDbWsSession } from 'drizzle-orm/netlify-db/session';

function isInternalWsSession(s: unknown): boolean {
  return s instanceof NetlifyDbWsSession;
}

// ensure nesting goes through the transaction object, never db.session
function assertNotInternalSession(target: unknown) {
  if (isInternalWsSession(target)) {
    throw new Error('Call tx.transaction() on the NetlifyDbTransaction, not on the internal session.');
  }
}

Type guard

import { NetlifyDbWsSession } from 'drizzle-orm/netlify-db/session';

function isNetlifyWsSession(s: unknown): s is NetlifyDbWsSession<any, any> {
  return s instanceof NetlifyDbWsSession;
}

Prevention

When it happens

Trigger: Only reachable through internal misuse — calling transaction() on the private NetlifyDbWsSession rather than on the NetlifyDbTransaction handed to the db.transaction() callback. Normal nested usage (tx.transaction(...) inside db.transaction()) hits NetlifyDbTransaction.transaction() and works correctly via savepoints.

Common situations: Not a user-facing error in correct usage. Would appear only in tests or custom code that grabs the internal session object and calls .transaction() on it. Seeing it means the wrong transaction entry point was used.

Related errors


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