fix: route $derived teardown errors through invoke_error_boundary#18486
fix: route $derived teardown errors through invoke_error_boundary#18486BonchitoSky wants to merge 3 commits into
Conversation
Two bugs fixed: 1. invoke_error_boundary only guarded the initial effect against DESTROYED, not ancestors walked in the loop. A fully-destroyed boundary has effect.b nulled, causing "Cannot read properties of null (reading 'error')" which masks the real error. Fix: move the guard inside the loop and also skip DESTROYING effects (mid-teardown). 2. execute_effect_teardown let teardown errors escape raw up the call stack, bypassing invoke_error_boundary entirely so <svelte:boundary onerror> was never called. Fix: catch and route through invoke_error_boundary. Fixes sveltejs#18485 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: c3fc325 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary of changesRoot causesThis PR fixes two distinct but related bugs that manifest when a Bug 1 —
|
…sertion
- Replace `onerror={(e) => errors.push(e.message)}` with `onerror={() => {}}`
in main.svelte; `errors` was an undeclared global (undefined at runtime),
disconnected from the dead `const errors = []` in _config.js.
- Remove the dead `const errors = []` declaration and its stale comment.
- Wrap the unmount flushSync in try/catch: when the boundary is torn down
together with the component all ancestor boundary effects carry DESTROYING,
so invoke_error_boundary finds no live handler and re-throws synchronously.
The real TypeError (null.value) escaping is acceptable.
- Tighten the assertion to match only "reading 'error'" — the specific
null-dereference crash (sveltejs#18485) this test guards against — rather than
the overly broad "Cannot read properties of null" which also matched
the original application error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes #18485
Problem
Two related bugs in error handling during effect teardown:
Bug 1 —
invoke_error_boundarycrashes withnull effect.bThe
DESTROYEDguard was only applied to the initial effect, not to ancestor effects encountered while walking up the tree. A boundary effect that has been fully destroyed haseffect.b = null(nulled at the end ofdestroy_effect), so callingeffect.b.error(error)inside the loop throws:This masks the original error and makes debugging very hard.
Bug 2 — teardown errors escape
<svelte:boundary>entirelyexecute_effect_teardownlet errors from teardown functions propagate raw up the JS call stack throughdestroy_effect→destroy_effect_children→ ..., bypassinginvoke_error_boundarycompletely. The<svelte:boundary onerror>handler was never called.Fix
error-handling.js: Move theDESTROYEDguard inside thewhileloop and extend it to also skipDESTROYINGeffects (mid-teardown, children being destroyed but not yet fully done). This prevents the null dereference and correctly skips boundaries that can no longer handle errors.effects.js: Wrapteardown.call(null)in acatchand route errors throughinvoke_error_boundary(error, effect.parent). Combined with the loop fix, destroyed/destroying ancestor boundaries are transparently skipped — the error either reaches a live ancestor boundary or surfaces at the top level.Test
Added
error-boundary-28— covers the minimal reproduction from the issue: a$derivedthat becomes dirty (without throwing) while mounted, then re-executes and throws during teardown when a child effect reads it.Before this fix:
TypeError: Cannot read properties of null (reading 'error')was thrown, masking the real error.After this fix: the real error propagates cleanly.