n8n-io/n8n · error · OptimisticLockCanNotBeUsedError

The optimistic lock can be used only with getOne() method.

Error message

The optimistic lock can be used only with getOne() method.

What it means

Thrown as OptimisticLockCanNotBeUsedError from getRawMany (and therefore getRawOne, which delegates to it) when expressionMap.lockMode === 'optimistic'. Optimistic locking requires materializing entity metadata to compare version columns, which the raw-result path does not do, so it refuses to run.

Source

Thrown at packages/@n8n/typeorm/src/query-builder/SelectQueryBuilder.ts:1475

	 * Disables the global condition of "non-deleted" for the entity with delete date columns.
	 */
	withDeleted(): this {
		this.expressionMap.withDeleted = true;
		return this;
	}

	/**
	 * Gets first raw result returned by execution of generated query builder sql.
	 */
	async getRawOne<T = any>(): Promise<T | undefined> {
		return (await this.getRawMany())[0];
	}

	/**
	 * Gets all raw results returned by execution of generated query builder sql.
	 */
	async getRawMany<T = any>(): Promise<T[]> {
		if (this.expressionMap.lockMode === 'optimistic') throw new OptimisticLockCanNotBeUsedError();

		this.expressionMap.queryEntity = false;
		const queryRunner = this.obtainQueryRunner();
		let transactionStartedByUs: boolean = false;
		try {
			// start transaction if it was enabled
			if (this.expressionMap.useTransaction === true && queryRunner.isTransactionActive === false) {
				await queryRunner.startTransaction();
				transactionStartedByUs = true;
			}

			const results = await this.loadRawResults(queryRunner);

			// close transaction if we started it
			if (transactionStartedByUs) {
				await queryRunner.commitTransaction();
			}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Use getOne() (or getMany()) instead of getRawMany()/getRawOne() when optimistic locking is set.
  2. If you need raw results, remove the optimistic lock for that query path: split into two query builders.
  3. Assert lockMode before choosing the read method so the mismatch is caught in tests.

Example fix

// before
qb.setLock('optimistic', version).getRawOne();
// after
qb.setLock('optimistic', version).getOne();
Defensive patterns

Strategy: validation

Validate before calling

function assertSafeForRaw(qb: SelectQueryBuilder<any>): void {
  // Optimistic locks cannot be used with raw reads
  if ((qb as any).expressionMap?.lockMode === 'optimistic') {
    throw new Error('Optimistic lock is incompatible with getRawMany/getRawOne; use getOne/getMany or drop the lock.');
  }
}

Type guard

function isOptimisticLockQuery(qb: { expressionMap?: { lockMode?: string } }): boolean {
  return qb.expressionMap?.lockMode === 'optimistic';
}

Try / catch

try {
  return await qb.getRawMany();
} catch (e) {
  if (e?.name === 'OptimisticLockCanNotBeUsedError' || /optimistic lock/i.test(e?.message ?? '')) {
    return await qb.getMany(); // fall back to entity read
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `.setLock('optimistic', version).getRawMany()` or `.getRawOne()`. Mixing `.setLock('optimistic', ...)` with raw projections.

Common situations: Refactoring a query from getOne() to getRawMany() for performance while leaving an optimistic-lock set; copy-paste of a query-builder chain across read paths; enabling optimistic locking globally in a base repository method that is also used for raw reads.

Related errors


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