Skip to content

🐞 fix: Reject runAsync startup and resumption failures - #5

Draft
Snowflyt wants to merge 1 commit into
mainfrom
fix/async-error-settlement
Draft

Snowflyt wants to merge 1 commit into
mainfrom
fix/async-error-settlement

Conversation

@Snowflyt

@Snowflyt Snowflyt commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

Keep startup and resumption failures connected to the public runAsync() promise.

Iterator construction currently happens before the promise executor, so an iterator factory can throw directly from runAsync(). During execution, the existing catches cover only two iterator.next() calls. Default-handler invocation, continuation registration through onComplete, and interrupt-time iterator advancement can throw outside those catches. After an asynchronous boundary, that throw can reject a discarded continuation promise while leaving the public promise pending.

Move iterator construction into the promise executor and use one catch around each complete call to iterate. The synchronous-effect loop remains iterative.

Regression coverage

  • A direct iterator factory and an Effected.all child factory throwing
  • Plain and generator default handlers throwing after an asynchronous boundary
  • Callback-based resumption followed by a throwing default handler
  • Ordinary and interruptable asynchronous registration throwing, before and after an asynchronous boundary
  • Interrupt-time iterator advancement throwing, before and after an asynchronous boundary
  • Existing synchronous runSync() throws and error identity preserved on rejection

Assertions await the public promise and compare its rejection reason with the original error. The baseline also records the independent unhandled rejections created by the escaped continuation throws.

Validation

Node v24.19.0, Vitest 3.0.9, TypeScript 5.8.2.

  • The base fails 8 of the 13 regression/control cases: 2 synchronous factory throws, 1 escaped callback-resumption throw, and 5 public promises timing out. Vitest independently reports 5 unhandled continuation rejections. The 5 control cases pass.
  • Candidate regression/control cases: 13/13 passed.
  • Complete test/effected.spec.ts: 129/129 passed.
  • Typecheck, ESLint, and formatting checks passed; type proofs: 37/37 passed.

The repository-wide runtime suite was not part of this candidate check.

Scope

runSync() is unchanged. This fix rejects the public promise when execution fails; iterator throw/return propagation and generator cleanup need their own protocol decisions.

Related drafts

The default-termination repair in #4 touches the same runner. The local combined settlement candidate passes all 137 tests in test/effected.spec.ts; preserve its termination checks when combining the drafts.

CI

CI run 39260511275 passed for 0a9458d62793c971be74ae534dd8645c931806f2: lint, typecheck, and type-proof/runtime tests on Node 18, 20, and 22.

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.

1 participant