Skip to content

✨ throw CircularTaskError when a task depends on itself or an ancestor#1162

Open
cowboyd wants to merge 1 commit intov4from
tests/no-circular-tasks
Open

✨ throw CircularTaskError when a task depends on itself or an ancestor#1162
cowboyd wants to merge 1 commit intov4from
tests/no-circular-tasks

Conversation

@cowboyd
Copy link
Copy Markdown
Member

@cowboyd cowboyd commented May 4, 2026

Motivation

A task that awaits its own result or the result of an ancestor is a circular dependency that should deadlock. The parent cannot complete until every child does, so any descendant waiting on itself or an ancestor should hang forever. This applies equally to the main task computation as well as its halt()

We currently try to fudge this situation when we encounter it and try to allow it to muddle through for historical reasons, but that ends up making things works by introducing complexity in order to paper over a pathological use-case. It's better to be up-front about it and when we detect a circular dependency between task, explicitly call that out

The options are:

  1. Silently deadlock
  2. try to do some fake workarounds (what is happening today)
  3. Fail fast when an impossible situation is detected.

Approach

Compare the scopes of the operation yielding to a task and the task itself. If the caller is, or is descended from the task being halted, then raise a CircularTaskError.

Potential Drawbacks

⚠️ this can break some code that could conceivably rely on this behavior, so it should not be merged without discussion. Personally, I think this can be released as a 4.1 concern since it does away with a totally invalid use-case which I have never personally encountered. However, it is worth considering.

A task may not wait on its own result or its own halt(), nor on any of
its ancestors' — by structured concurrency the parent cannot complete
until every child does, so any descendant waiting on an ancestor is a
circular dependency that would deadlock.

Detect synchronously inside the task[Symbol.iterator] and halt
thenable[Symbol.iterator] wrappers via assertNonCircular: walk the
calling scope's contexts prototype chain and throw if the targeted
task's scope appears in it. Sibling and downward (parent→child) waits
are unaffected.

Expose createFuture's operation directly on FutureWithResolvers so the
task iterator wrapper can yield to it without re-entering its own
overridden Symbol.iterator (which would recurse).
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 4, 2026

Open in StackBlitz

npm i https://pkg.pr.new/effection@1162

commit: 7a13293

@cowboyd cowboyd requested review from jbolda and taras May 4, 2026 16:55
@cowboyd cowboyd self-assigned this May 4, 2026
@cowboyd cowboyd linked an issue May 4, 2026 that may be closed by this pull request
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.

yield* task.halt() from the same task is a self-join

1 participant