n8n-io/n8n · error · TransactionRollbackFailedError

Transaction rollback failed

Error message

Transaction rollback failed

What it means

Thrown by the abstract SQLite query runner when the underlying `ROLLBACK` (or `ROLLBACK TO SAVEPOINT`) SQL statement itself fails inside rollbackTransaction(). The original driver error is passed as `error.cause` (see TransactionRollbackFailedError). It means SQLite could not undo the transaction — almost always a low-level I/O or connection-state problem, not a bug in your migration logic.

Source

Thrown at packages/@n8n/typeorm/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts:156

	 */
	async rollbackTransaction(): Promise<void> {
		try {
			if (!this.isTransactionActive) throw new TransactionNotStartedError();

			await this.broadcaster.broadcast('BeforeTransactionRollback');

			if (this.transactionDepth > 1) {
				this.transactionDepth -= 1;
				await this.query(`ROLLBACK TO SAVEPOINT typeorm_${this.transactionDepth}`);
			} else {
				this.transactionDepth -= 1;
				await this.query('ROLLBACK');
				this.isTransactionActive = false;
			}

			await this.broadcaster.broadcast('AfterTransactionRollback');
		} catch (rollbackError) {
			throw new TransactionRollbackFailedError(rollbackError);
		}
	}

	/**
	 * Returns raw data stream.
	 */
	stream(
		query: string,
		parameters?: any[],
		onEnd?: Function,
		onError?: Function,
	): Promise<ReadStream> {
		throw new TypeORMError(`Stream is not supported by sqlite driver.`);
	}

	/**
	 * Returns all available database names including system databases.
	 */

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Inspect `error.cause` for the real SQLite error code/message (SQLITE_IOERR, SQLITE_BUSY, 'no such savepoint', 'database is locked') and address that root cause.
  2. Keep the DB file on stable local storage for the process lifetime.
  3. If triggered by a failed prior statement, fix that statement so the transaction stays valid.
  4. Re-open the DataSource / restart the process — after a failed rollback the connection is typically wedged and must not be reused.

Example fix

// before
await queryRunner.rollbackTransaction();

// after
try {
  await queryRunner.rollbackTransaction();
} catch (e) {
  if (e instanceof TransactionRollbackFailedError) {
    logger.error('rollback failed', { cause: e.cause });
    await dataSource.destroy(); // connection is unusable
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// No safe pre-check: rollback fails at the SQL layer.
// Ensure the connection is healthy before attempting:
if (!queryRunner.connection.isInitialized) {
  throw new Error('DataSource not initialized — cannot rollback');
}

Type guard

import { TypeORMError } from '@n8n/typeorm';

function isTransactionRollbackFailedError(e: unknown): e is TypeORMError & { cause: unknown } {
  return e instanceof TypeORMError && e.message === 'Transaction rollback failed';
}

Try / catch

try {
  await queryRunner.rollbackTransaction();
} catch (e) {
  if (isTransactionRollbackFailedError(e)) {
    logger.error('sqlite rollback failed', { cause: e.cause });
    // connection is wedged — tear it down
    await dataSource.destroy().catch(() => {});
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling queryRunner.rollbackTransaction() on a SQLite AbstractSqliteQueryRunner after the connection hit a disk I/O error, the DB file was removed/unreachable, or an earlier statement already broke the transaction. Also when a nested savepoint rollback fails (transactionDepth > 1, e.g. 'no such savepoint: typeorm_0').

Common situations: SQLite DB file on an unreliable/network mount, disk full, file deleted or moved while the process runs, concurrent write-lock escalation corrupting txn state, or code that manually fiddles with the same sqlite3 connection outside TypeORM.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/f86cdc0c305375df. Report an issue: GitHub.