chartjs/Chart.js · error · Error
Recursion detected: ${Array.from(_stack).join('->')}->${prop
Error message
Recursion detected: ${Array.from(_stack).join('->')}->${prop} What it means
When a Chart.js option is a scriptable function, the config resolver invokes it and tracks the property name in a per-context _stack to detect cycles. If resolving a scriptable option references itself (directly or through sub-resolvers), _stack.has(prop) becomes true and the resolver throws to prevent infinite recursion / stack overflow. This protects option resolution for scriptable values that read other options via the resolver proxy.
Source
Thrown at src/helpers/helpers.config.ts:263
if (isArray(value) && value.length) {
value = _resolveArray(prop, value, target, descriptors.isIndexable);
}
if (needsSubResolver(prop, value)) {
// if the resolved value is an object, create a sub resolver for it
value = _attachContext(value, _context, _subProxy && _subProxy[prop], descriptors);
}
return value;
}
function _resolveScriptable(
prop: string,
getValue: (ctx: AnyObject, sub: AnyObject) => unknown,
target: ContextCache,
receiver: AnyObject
) {
const {_proxy, _context, _subProxy, _stack} = target;
if (_stack.has(prop)) {
throw new Error('Recursion detected: ' + Array.from(_stack).join('->') + '->' + prop);
}
_stack.add(prop);
let value = getValue(_context, _subProxy || receiver);
_stack.delete(prop);
if (needsSubResolver(prop, value)) {
// When scriptable option returns an object, create a resolver on that.
value = createSubResolver(_proxy._scopes, _proxy, prop, value);
}
return value;
}
function _resolveArray(
prop: string,
value: unknown[],
target: ContextCache,
isIndexable: (key: string) => boolean
) {
const {_proxy, _context, _subProxy, _descriptors: descriptors} = target;View on GitHub (pinned to cb02e1d207)
Solutions
- Read the value from a DIFFERENT, non-scriptable option or from the raw data/context instead of re-entering the same key.
- Hoist the constant out of the function: compute the base value once outside the scriptable closure.
- Break the cycle by accessing ctx.parsed / ctx.dataset / raw data rather than ctx.chart.options.<sameKey>.
- Audit the resolution chain shown in the message (a->b->...->prop) and remove the back-reference.
Example fix
// before - borderColor reads itself via options -> recursion options.borderColor = (ctx) => ctx.chart.options.borderColor; // after - read from a separate config value or data options.borderColor = (ctx) => ctx.dataset.borderColor || '#000';
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-scan scriptable options for self-references before applying the config (heuristic).
function detectSelfReference(optionFns) {
for (const [key, fn] of Object.entries(optionFns)) {
const src = String(fn);
// crude check: does the function body re-read the same option key?
if (new RegExp(`\\boptions\\.${key}\\b`).test(src)) {
console.warn(`Scriptable option '${key}' appears to reference itself; this may recurse.`);
}
}
} Try / catch
// Wrap option resolution in tests to surface recursion as a readable failure.
try {
chart.update();
} catch (e) {
if (/Recursion detected/.test(String(e?.message))) {
console.error('Scriptable option cycle:', e.message);
// strip scriptable functions and retry to isolate the offender
} else {
throw e;
}
} Prevention
- Never read the same option key from ctx.chart.options inside its own scriptable function.
- Prefer reading ctx.parsed / ctx.dataset / ctx.raw over re-entering options.
- Hoist constants outside the scriptable closure when the value does not depend on context.
- When a scriptable option must depend on another option, ensure that other option is non-scriptable.
When it happens
Trigger: A scriptable option function that reads the same option (or a chain leading back to it) from the context/sub-proxy, e.g. options.borderColor = (ctx) => ctx.chart.options.borderColor; returning an object that re-triggers resolution of the same key; mutual references between two scriptable options.
Common situations: Writing a scriptable color/size that references ctx.chart.options.X where X is the same option; spreading the resolver into itself; sub-options that loop back through needsSubResolver; misusing the scriptable context's subProxy to read parent options that recurse.
Related errors
- This method is not implemented: Check that a complete date a
- Cannot determine type of '${id}' axis. Please provide 'axis'
- "${id}" is not a registered ${type}.
- class does not have id: ${item}
- Unsupported decimation algorithm '${options.algorithm}'
AI-assisted analysis of chartjs/Chart.js@cb02e1d207 (2026-08-12).
Data as JSON: /api/errors/96870d29ba38ba4f.
Report an issue: GitHub.