Skip to content

fix(core): defer fatal handlers outside event dispatch#268

Merged
RtlZeroMemory merged 2 commits intomainfrom
fix/fatal-handler-inline-reentrancy
Mar 6, 2026
Merged

fix(core): defer fatal handlers outside event dispatch#268
RtlZeroMemory merged 2 commits intomainfrom
fix/fatal-handler-inline-reentrancy

Conversation

@RtlZeroMemory
Copy link
Owner

@RtlZeroMemory RtlZeroMemory commented Mar 6, 2026

Summary

  • defer inline fatal dispatch until app user-code depth unwinds
  • add a regression test proving fatal handlers can call \ after an \ handler throw

Why

CodeRabbit flagged a real runtime hole in merged PR #265: fatal handlers could run while , causing app API calls from fatal handlers to trip re-entrancy guards and get swallowed.

Validation

  • npm run lint
  • npm run build
  • node scripts/run-tests.mjs --filter "onEventHandlers|fatal|eventPump"

Summary by CodeRabbit

  • Bug Fixes

    • Deferred fatal conditions during event handling are now processed after the event stack unwinds, ensuring fatal handlers run reliably and disposal can occur without error.
  • Tests

    • Added tests verifying that failures inside event handlers prevent same-turn updates and that fatal handlers run after the handler stack unwinds.

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 677c2037-1e28-428e-a649-e757d619e9f6

📥 Commits

Reviewing files that changed from the base of the PR and between 92dfdfb and eeb8e16.

📒 Files selected for processing (2)
  • packages/core/src/app/__tests__/onEventHandlers.test.ts
  • packages/core/src/app/createApp.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/app/tests/onEventHandlers.test.ts

📝 Walkthrough

Walkthrough

Defers certain fatal errors that occur inline during event handling until the onEvent stack unwinds, adds a flush mechanism to run deferred fatals after event processing completes, and adds two tests verifying deferred fatal behavior and that fatal handlers run after event-depth unwinds.

Changes

Cohort / File(s) Summary
Event handler tests
packages/core/src/app/__tests__/onEventHandlers.test.ts
Adds two tests: one ensuring onEvent handler failures prevent same-turn queued updates and stop/dispose the backend; another verifying fatal handlers run after the onEvent call stack unwinds and that dispose can be called from the fatal handler.
Fatal handling control flow
packages/core/src/app/createApp.ts
Introduces deferredInlineFatal storage and flushDeferredInlineFatal. Updates fatalNowOrEnqueue to compute canFailFastInline = scheduler.isExecuting && !inRender && !inCommit and: if inline-capable and inside event-handler depth, defer fatal to deferredInlineFatal; if inline-capable and not in event depth, call doFatal immediately; otherwise use existing enqueueFatal path. Ensures flushDeferredInlineFatal is invoked after event emission and when event-depth unwinds so deferred fatals run. No public API/signature changes.

Sequence Diagram(s)

sequenceDiagram
  participant Emitter as EventEmitter
  participant Scheduler as Scheduler
  participant App as createApp
  participant Fatal as FatalHandler

  Emitter->>App: emit(text event)
  App->>Scheduler: mark isExecuting / inEventHandlerDepth++
  App->>App: run onEvent handler (throws)
  App->>App: fatalNowOrEnqueue(fatal)
  alt canFailFastInline && inEventHandlerDepth > 0
    App-->>App: store deferredInlineFatal
  else canFailFastInline && inEventHandlerDepth == 0
    App->>Fatal: doFatal (run fatal handlers)
  else
    App->>Scheduler: enqueueFatal (fallback)
  end
  App->>Scheduler: inEventHandlerDepth-- (unwind)
  App->>App: flushDeferredInlineFatal()
  App->>Fatal: execute deferred fatal handlers (dispose allowed)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through handlers, brief and spry,

a throw went up, the stack ran high.
I tucked the fatal in a cozy den,
then waited till the calls came then.
Flush! — I nibbled bugs away, and closed the glen.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(core): defer fatal handlers outside event dispatch' directly and specifically describes the main change: deferring fatal handler execution until after event dispatch completes, which aligns with the core implementation change in createApp.ts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/fatal-handler-inline-reentrancy

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92dfdfb106

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +885 to +887
const canFailFastInline =
scheduler.isExecuting && !inRender && !inCommit && inEventHandlerDepth === 0;
if (canFailFastInline) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve fail-fast when fatal is raised in event handlers

Gating inline fatal dispatch on inEventHandlerDepth === 0 means an onEvent throw now queues fatal for the next turn instead of faulting immediately; while that queued item waits, processTurn continues handling the rest of the current turn (including additional coalesced eventBatch items and commitUpdates/tryRenderOnce) because state is still Running. In turns that already contain multiple queued items, this allows post-error events and state changes to run before the fatal transition, which breaks the previous fail-fast behavior for terminal user-code errors.

Useful? React with 👍 / 👎.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in eeb8e16. Fatal raised during event handlers now defers only until inEventHandlerDepth unwinds, then faults in the same turn before queued updates can commit. Added regression coverage in packages/core/src/app/__tests__/onEventHandlers.test.ts for same-turn queued updater execution and fatal-handler dispose() after onEvent throws.

@RtlZeroMemory RtlZeroMemory merged commit 21a9522 into main Mar 6, 2026
28 of 32 checks passed
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