n8n-io/n8n · error · IsolateError

Evaluator disposed

Error message

Evaluator disposed

What it means

Thrown as IsolateError('Evaluator disposed') by ExpressionEvaluator.evaluate() when this.disposed is true — i.e. evaluate() is called after dispose(). dispose() sets the flag, clears the code cache, and disposes the bridge pool; any subsequent evaluate() is a use-after-dispose bug. IsolateError is imported from @n8n/errors.

Source

Thrown at packages/@n8n/expression-runtime/src/evaluator/expression-evaluator.ts:113

		try {
			bridge = this.pool.acquire();
		} catch (error) {
			if (error instanceof PoolDisposedError) throw error;
			if (!(error instanceof PoolExhaustedError)) throw error;
			bridge = await this.createBridge();
		}
		this.config.observability?.metrics.counter(EXPRESSION_METRICS.poolAcquired.name, 1);
		this.bridgesByCaller.set(caller, bridge);
		return true;
	}

	evaluate(
		expression: string,
		data: WorkflowData,
		caller: object,
		options?: EvaluateOptions,
	): unknown {
		if (this.disposed) throw new IsolateError('Evaluator disposed');

		const bridge = this.getBridge(caller);

		// Transform template expression → sanitized JavaScript (cached)
		const transformedCode = this.getTransformedCode(expression);

		const { observability } = this.config;
		const start = performance.now();

		try {
			const result = bridge.execute(transformedCode, data, {
				timezone: options?.timezone,
			});
			recordOutcome(observability, start, 'success');
			return result;
		} catch (error) {
			recordOutcome(observability, start, 'error', error);
			throw error;

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Check evaluator.isDisposed() before calling evaluate() in code paths that may outlive the evaluator.
  2. Ensure dispose() is only called after all in-flight evaluations have completed (drain before dispose).
  3. In tests, await pending evaluations before invoking evaluator.dispose() in afterEach.
  4. Give each long-lived consumer its own evaluator, or use reference counting, so one consumer's dispose doesn't break others.

Example fix

// before — dispose races an in-flight evaluation
evaluator.dispose();
const result = evaluator.evaluate('{{ $json.x }}', data, caller); // throws
// after — guard with isDisposed
if (!evaluator.isDisposed()) {
  const result = evaluator.evaluate('{{ $json.x }}', data, caller);
}
Defensive patterns

Strategy: validation

Validate before calling

function evaluateIfAlive(evaluator, expr, data, caller) {
  if (evaluator.isDisposed()) throw new Error('Evaluator is disposed; create a new one');
  return evaluator.evaluate(expr, data, caller);
}

Type guard

function isEvaluatorUsable(evaluator) { return !evaluator.isDisposed(); }

Try / catch

import { IsolateError } from '@n8n/errors';
try {
  const result = evaluator.evaluate(expr, data, caller);
} catch (e) {
  if (e instanceof IsolateError && /disposed/i.test(e.message)) {
    // create a fresh evaluator or skip this evaluation
  } else throw e;
}

Prevention

When it happens

Trigger: Calling evaluator.evaluate(expression, data, caller) after await evaluator.dispose() has run. Common in shutdown races: a workflow finalizer disposes the evaluator while an in-flight expression is still being evaluated, or a test tears down the evaluator before an async evaluation completes.

Common situations: Test teardown disposing the evaluator before pending evaluations finish. Application shutdown disposing shared evaluator while workers still reference it. A bug where dispose() is called on the wrong lifecycle event.

Related errors


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