Skip to content

Fix stack overflow when chaining an already chained wrapper#3021

Open
chatman-media wants to merge 1 commit into
jashkenas:masterfrom
chatman-media:fix/double-chain-stack-overflow
Open

Fix stack overflow when chaining an already chained wrapper#3021
chatman-media wants to merge 1 commit into
jashkenas:masterfrom
chatman-media:fix/double-chain-stack-overflow

Conversation

@chatman-media

Copy link
Copy Markdown

Summary

Fixes #2853.

_.chain(value).chain() — or any redundant .chain() call within a chained sequence — overflows the stack with RangeError: Maximum call stack size exceeded instead of behaving like a no-op and returning a wrapper of the same value.

_.chain([1]).chain();             // RangeError: Maximum call stack size exceeded
_([1]).chain().chain().value();   // RangeError: Maximum call stack size exceeded

Root cause

chainResult continues chaining intermediate results via _(obj).chain(). When obj is the value returned by the standalone chain function, it is already a wrapped _ instance. _(obj) therefore returns that same wrapper (per if (obj instanceof _) return obj;), and invoking the prototype chain method on it re-enters chainResult, which re-chains again — recursing without a base case until the stack overflows.

The recursion loop is: prototype chain → standalone chain (produces a wrapper) → chainResult re-chains that wrapper via prototype chain → repeat.

As noted in the issue, this was introduced together with #1691.

Fix

Call the standalone chain helper from chainResult instead of _(obj).chain(). chain sets the _chain flag on the (possibly already wrapped) value directly and returns, so chaining an already chained wrapper is idempotent and terminates:

return instance._chain ? chain(obj) : obj;

This is safe for every chainResult call site: the array-method wrappers pass a plain (unwrapped) value, and tap returns the unwrapped value too — only _.chain returns an _ instance, which is exactly the case that previously recursed.

Tests

Added a regression test in test/chaining.js (#2853). It verifies the redundant .chain() no longer throws, preserves the wrapped value, stays chained, and that further chaining keeps working.

  • Without the fix the new test fails with Maximum call stack size exceeded (verified).
  • With the fix the full suite passes (209 assertions) and eslint is clean.

No public API change; _.chain([1]).chain() now returns a chained wrapper of [1] as expected.

`_.chain(value).chain()` (or any redundant `.chain()` call in a chained
sequence) overflowed the stack instead of behaving like a no-op.

`chainResult` continued chaining via `_(obj).chain()`. When `obj` is the
result of the standalone `chain` function it is already a wrapped instance,
so `_(obj)` returns that same wrapper and invoking the prototype `chain`
method re-enters `chainResult`, recursing without end.

Call the standalone `chain` helper instead, which sets the `_chain` flag on
the (possibly already wrapped) value directly and terminates. Refs jashkenas#2853.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Double chaining leads to a stack overflow

1 participant