{"record":{"id":"410b50806295608a","repo":"n8n-io/n8n","slug":"evaluator-disposed","errorCode":null,"errorMessage":"Evaluator disposed","messagePattern":"Evaluator disposed","errorType":"exception","errorClass":"IsolateError","httpStatus":null,"severity":"error","filePath":"packages/@n8n/expression-runtime/src/evaluator/expression-evaluator.ts","lineNumber":113,"sourceCode":"\t\ttry {\n\t\t\tbridge = this.pool.acquire();\n\t\t} catch (error) {\n\t\t\tif (error instanceof PoolDisposedError) throw error;\n\t\t\tif (!(error instanceof PoolExhaustedError)) throw error;\n\t\t\tbridge = await this.createBridge();\n\t\t}\n\t\tthis.config.observability?.metrics.counter(EXPRESSION_METRICS.poolAcquired.name, 1);\n\t\tthis.bridgesByCaller.set(caller, bridge);\n\t\treturn true;\n\t}\n\n\tevaluate(\n\t\texpression: string,\n\t\tdata: WorkflowData,\n\t\tcaller: object,\n\t\toptions?: EvaluateOptions,\n\t): unknown {\n\t\tif (this.disposed) throw new IsolateError('Evaluator disposed');\n\n\t\tconst bridge = this.getBridge(caller);\n\n\t\t// Transform template expression → sanitized JavaScript (cached)\n\t\tconst transformedCode = this.getTransformedCode(expression);\n\n\t\tconst { observability } = this.config;\n\t\tconst start = performance.now();\n\n\t\ttry {\n\t\t\tconst result = bridge.execute(transformedCode, data, {\n\t\t\t\ttimezone: options?.timezone,\n\t\t\t});\n\t\t\trecordOutcome(observability, start, 'success');\n\t\t\treturn result;\n\t\t} catch (error) {\n\t\t\trecordOutcome(observability, start, 'error', error);\n\t\t\tthrow error;","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/expression-runtime/src/evaluator/expression-evaluator.ts#L95-L131","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check evaluator.isDisposed() before calling evaluate() in code paths that may outlive the evaluator.","Ensure dispose() is only called after all in-flight evaluations have completed (drain before dispose).","In tests, await pending evaluations before invoking evaluator.dispose() in afterEach.","Give each long-lived consumer its own evaluator, or use reference counting, so one consumer's dispose doesn't break others."],"exampleFix":"// before — dispose races an in-flight evaluation\nevaluator.dispose();\nconst result = evaluator.evaluate('{{ $json.x }}', data, caller); // throws\n// after — guard with isDisposed\nif (!evaluator.isDisposed()) {\n  const result = evaluator.evaluate('{{ $json.x }}', data, caller);\n}","handlingStrategy":"validation","validationCode":"function evaluateIfAlive(evaluator, expr, data, caller) {\n  if (evaluator.isDisposed()) throw new Error('Evaluator is disposed; create a new one');\n  return evaluator.evaluate(expr, data, caller);\n}","typeGuard":"function isEvaluatorUsable(evaluator) { return !evaluator.isDisposed(); }","tryCatchPattern":"import { IsolateError } from '@n8n/errors';\ntry {\n  const result = evaluator.evaluate(expr, data, caller);\n} catch (e) {\n  if (e instanceof IsolateError && /disposed/i.test(e.message)) {\n    // create a fresh evaluator or skip this evaluation\n  } else throw e;\n}","preventionTips":["Check evaluator.isDisposed() before calling evaluate() in long-lived or async paths.","Drain all in-flight evaluations before calling dispose().","In tests, await pending work before evaluator.dispose() in afterEach.","Avoid sharing one evaluator across consumers with different lifecycles."],"tags":["expression-runtime","lifecycle","use-after-dispose","evaluator"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}