Skip to content

fix(Deferred): guard waiter cleanup against post-completion runs - #7193

Merged
tim-smart merged 2 commits into
Effect-TS:mainfrom
kitlangton:deferred-cleanup-after-completion
Aug 12, 2026
Merged

tim-smart merged 2 commits into
Effect-TS:mainfrom
kitlangton:deferred-cleanup-after-completion

Conversation

@kitlangton

@kitlangton kitlangton commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #7194

What

Interrupting a Deferred.await waiter after the Deferred has completed dies with TypeError: undefined is not an object (evaluating 'self.resumes.indexOf') — the defect replaces the interrupt cause, so the fiber exits Die instead of interrupted. Observed in production at 4.0.0-beta.101 (long-poll handler, request abort racing a concurrent completion → HTTP 500s); code unchanged at head.

Before / After

Before: _await's cleanup unconditionally dereferenced self.resumes, but doneUnsafe sets it to undefined after resuming waiters — so any cleanup running post-completion crashed.

After: the cleanup early-returns when resumes is already cleared (and splices only on a found index, so a stale cleanup can never remove another waiter's registration via splice(-1, 1)).

sequenceDiagram
    participant W as Waiter fiber
    participant D as Deferred
    participant I as Interruptor
    W->>D: await — push resume, get cleanup
    I->>D: complete (doneUnsafe)
    D->>W: resume(effect) — waiter suspends in completion effect
    D->>D: resumes = undefined
    I->>W: interruptUnsafe()
    W->>D: cleanup: resumes.indexOf ❌ TypeError → Die
    Note over W,D: after fix: cleanup sees resumes cleared, returns — waiter exits interrupted ✓
Loading

Testing

New test: await - interrupting a waiter after completion does not die. Deterministic: the completion effect suspends (yieldNow), so the resumed waiter's async-finalizer frame is still on its stack when it is interrupted, which runs the cleanup post-completion. Fails on the unguarded code with exactly the production defect; passes with the guard (verified both ways).

  • pnpm vitest run --project effect — 8104 passed; the one failure (unstable/cli/Prompt.test.ts, date formatting) is pre-existing on an unmodified tree
  • pnpm --dir packages/effect check, oxlint, dprint check — clean
  • Changeset: effect patch

@changeset-bot

changeset-bot Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04ea598

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
effect Patch
@effect/ai-anthropic Patch
@effect/ai-openai Patch
@effect/ai-openai-compat Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch
@effect/opentelemetry Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node Patch
@effect/platform-node-shared Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/vitest Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
basic.ts 6.91 KB 6.91 KB 0.00 KB (0.00%)
batching.ts 9.72 KB 9.72 KB 0.00 KB (0.00%)
brand.ts 6.50 KB 6.50 KB 0.00 KB (0.00%)
cache.ts 10.63 KB 10.63 KB 0.00 KB (0.00%)
config.ts 21.05 KB 21.05 KB 0.00 KB (0.00%)
differ.ts 19.99 KB 19.99 KB 0.00 KB (0.00%)
http-client.ts 21.56 KB 21.55 KB +0.01 KB (+0.05%)
logger.ts 10.88 KB 10.88 KB 0.00 KB (0.00%)
metric.ts 8.86 KB 8.86 KB 0.00 KB (0.00%)
optic.ts 6.67 KB 6.67 KB 0.00 KB (0.00%)
pubsub.ts 14.90 KB 14.90 KB +0.01 KB (+0.05%)
queue.ts 11.57 KB 11.57 KB 0.00 KB (0.00%)
schedule.ts 10.74 KB 10.74 KB 0.00 KB (0.00%)
schema-class.ts 19.61 KB 19.61 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 29.57 KB 29.57 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 25.81 KB 25.81 KB 0.00 KB (0.00%)
schema-string-transformation.ts 13.49 KB 13.49 KB 0.00 KB (0.00%)
schema-string.ts 10.99 KB 10.99 KB 0.00 KB (0.00%)
schema-template-literal.ts 15.30 KB 15.30 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 21.73 KB 21.73 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 24.20 KB 24.20 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 18.96 KB 18.96 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 18.78 KB 18.78 KB 0.00 KB (0.00%)
schema-toFormatter.ts 18.65 KB 18.65 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 22.81 KB 22.81 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 19.30 KB 19.30 KB 0.00 KB (0.00%)
schema.ts 18.87 KB 18.87 KB 0.00 KB (0.00%)
stm.ts 12.66 KB 12.66 KB 0.00 KB (0.00%)
stream.ts 9.66 KB 9.66 KB 0.00 KB (0.00%)

Completion resumes every waiter and sets `resumes` to undefined. A
waiter's interrupt cleanup that runs after completion dereferenced the
cleared array: TypeError: undefined is not an object (evaluating
'self.resumes.indexOf'). Observed in production at 4.0.0-beta.101
(long-poll HTTP handler, request abort interrupting the awaiting fiber);
the code is unchanged at head.
Waiters resume by running the completion effect, so suspending inside it
leaves the await cleanup on the waiter's stack after `doneUnsafe` has
cleared `resumes`. Interrupting the waiter from that tick runs the
cleanup post-completion, which died with

  TypeError: Cannot read properties of undefined (reading 'indexOf')

before the guard.
@kitlangton
kitlangton force-pushed the deferred-cleanup-after-completion branch from d1daa86 to 04ea598 Compare August 12, 2026 01:51
@kitlangton
kitlangton marked this pull request as ready for review August 12, 2026 01:51

This branch had an error being deployed

1 failed deployment
fork — 04ea5986 Deployed Aug 12, 2026 by kitlangton via approval-gate #11695
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.

Deferred.await waiter interrupted after completion dies with TypeError

2 participants