sveltejs / svelte · Issue No. 18584
Looking into lifecycle internals for #18583 led me here. If a server render throws partway through, onDestroy callbacks registered by components that already ran are never called: #close_render is the only caller of #collect_on_destroy and is unreachable on the error path. The abort-signal cleanup in the same finally blocks does run (renderer.js:643, renderer.js:669), so getAbortSignal().addEventListener('abort', ...) fires on failed renders while onDestroy doesn't.
#10296 added server onDestroy so per-request resources get released, and a failed render is where skipping that hurts most, since errors tend to repeat across requests. If success-only cleanup is intended, I couldn't find it documented.
Affects both the sync and async (top-level await) render paths.
<!-- Parent.svelte -->
<script>
import { onDestroy, getAbortSignal } from 'svelte';
import Child from './Child.svelte';
let { fail } = $props();
onDestroy(() => console.log('onDestroy fired'));
getAbortSignal().addEventListener('abort', () => console.log('abort fired'));
</script>
{#if fail}<Child />{/if}
<!-- Child.svelte -->
<script>
throw new Error('boom');
</script>
render(Parent, { props: { fail: false } }) logs both callbacks. With fail: true it logs only abort fired.
--- successful render ---
onDestroy fired
abort fired
--- failing render ---
abort fired
threw: boom
svelte 5.56.7, node 22.23.1
annoyance
Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.