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
- 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).
- Use the callback form `manager.transaction(async (em) => ...)` so TypeORM owns release; do not release the inner query runner yourself.
- If you create the queryRunner manually (`connection.createQueryRunner()`), release it exactly once in a `finally` and null the reference afterwards.
- 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
- Prefer the callback transaction() form so TypeORM manages release.
- Release manual query runners exactly once in finally, then null the reference.
- Never cache an EntityManager created from a request-scoped query runner beyond the request.
- Audit code paths where Promise.race or early returns could call release() twice.
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
- Driver not Connected
- Transactions aren't supported by ${this.connection.driver.op
- SQLite only supports SERIALIZABLE and READ UNCOMMITTED isola
- Transaction method requires callback in second parameter if
- Entity manager is not using single database connection and c
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/cc5828e1cc020469.
Report an issue: GitHub.