n8n-io/n8n · error · QueryRunnerProviderAlreadyReleasedError

Database connection provided by a query runner was already r

Error message

Database connection provided by a query runner was already released, cannot continue to use its querying methods anymore.

What it means

QueryRunnerProviderAlreadyReleasedError fires when EntityManager.transaction() (or any path) is invoked on a query runner whose isReleased flag is already true — i.e. release() was called on it earlier. The driver/connection underneath is gone, so further querying is impossible. The class is a TypeORMError subclass produced in EntityManager at line 136.

Source

Thrown at packages/@n8n/typeorm/src/entity-manager/EntityManager.ts:136

	async transaction<T>(
		isolationOrRunInTransaction: IsolationLevel | ((entityManager: EntityManager) => Promise<T>),
		runInTransactionParam?: (entityManager: EntityManager) => Promise<T>,
	): Promise<T> {
		const isolation =
			typeof isolationOrRunInTransaction === 'string' ? isolationOrRunInTransaction : undefined;
		const runInTransaction =
			typeof isolationOrRunInTransaction === 'function'
				? isolationOrRunInTransaction
				: runInTransactionParam;

		if (!runInTransaction) {
			throw new TypeORMError(
				`Transaction method requires callback in second parameter if isolation level is supplied.`,
			);
		}

		if (this.queryRunner && this.queryRunner.isReleased)
			throw new QueryRunnerProviderAlreadyReleasedError();

		// if query runner is already defined in this class, it means this entity manager was already created for a single connection
		// if its not defined we create a new query runner - single connection where we'll execute all our operations
		const queryRunner = this.queryRunner || this.connection.createQueryRunner();

		try {
			await queryRunner.startTransaction(isolation);
			const result = await runInTransaction(queryRunner.manager);
			await queryRunner.commitTransaction();
			return result;
		} catch (err) {
			try {
				// we throw original error even if rollback thrown an error
				await queryRunner.rollbackTransaction();
			} catch (rollbackError) {}
			throw err;
		} finally {
			if (!this.queryRunner)

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Audit every code path that calls `queryRunner.release()` and ensure no manager/queryRunner method runs after it (move release() to the true end of the unit of work).
  2. Use the callback form `manager.transaction(async (em) => ...)` so TypeORM owns release; do not release the inner query runner yourself.
  3. If you create the queryRunner manually (`connection.createQueryRunner()`), release it exactly once in a `finally` and null the reference afterwards.
  4. Avoid caching EntityManagers created from a query runner in singletons; obtain a fresh manager per unit of work.

Example fix

// before
const qr = connection.createQueryRunner();
await qr.startTransaction();
await qr.manager.save(User, u);
await qr.commitTransaction();
await qr.release();
// later, accidentally:
await qr.manager.find(User); // -> already released

// after - release exactly once, no further use
const qr = connection.createQueryRunner();
try {
  await qr.startTransaction();
  await qr.manager.save(User, u);
  await qr.commitTransaction();
} catch (e) {
  await qr.rollbackTransaction();
  throw e;
} finally {
  await qr.release();
}
// qr is never touched again
Defensive patterns

Strategy: validation

Validate before calling

// Before using a manager tied to a query runner, check its liveness
function isQueryRunnerUsable(qr: QueryRunner | undefined): boolean {
  return !!qr && !qr.isReleased;
}
if (!isQueryRunnerUsable(manager.queryRunner)) {
  throw new Error('Cannot use manager: query runner already released');
}

Type guard

function isReleasedError(e: unknown): boolean {
  return e instanceof Error && /already released/.test(e.message);
}

Try / catch

try {
  await manager.transaction(async (em) => em.save(u));
} catch (e) {
  if (isReleasedError(e)) {
    // the runner is gone — obtain a fresh one and retry, or fail the unit of work
    throw new OperationalError('query runner released mid-flight', { cause: e });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `manager.transaction()` (or any manager method) after `queryRunner.release()` was already invoked; reusing an EntityManager created from a single-connection query runner past its lifetime; closing the DataSource while a transaction is queued; holding a manager in a long-lived singleton that outlives its query runner.

Common situations: Manual query-runner usage where release() is called in a finally block and then code falls through to another manager call; DI of an EntityManager that was tied to a request-scoped query runner that the request already torn down; a Promise.race where the losing branch released the runner; integration tests sharing a runner across cases.

Related errors


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