diff --git a/.changeset/hip-forks-exercise.md b/.changeset/hip-forks-exercise.md new file mode 100644 index 0000000000..41df40bd6a --- /dev/null +++ b/.changeset/hip-forks-exercise.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: capture infinite_loop_guard in error boundary diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a3ec5bca34..4928419d16 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -48,6 +48,9 @@ let scheduler_mode = FLUSH_MICROTASK; // Used for handling scheduling let is_micro_task_queued = false; +/** @type {Effect | null} */ +let last_scheduled_effect = null; + export let is_flushing_effect = false; export let is_destroying_effect = false; @@ -532,27 +535,47 @@ export function update_effect(effect) { } } +function log_effect_stack() { + // eslint-disable-next-line no-console + console.error( + 'Last ten effects were: ', + dev_effect_stack.slice(-10).map((d) => d.fn) + ); + dev_effect_stack = []; +} + function infinite_loop_guard() { if (flush_count > 1000) { flush_count = 0; - if (DEV) { - try { - e.effect_update_depth_exceeded(); - } catch (error) { + try { + e.effect_update_depth_exceeded(); + } catch (error) { + if (DEV) { // stack is garbage, ignore. Instead add a console.error message. define_property(error, 'stack', { value: '' }); - // eslint-disable-next-line no-console - console.error( - 'Last ten effects were: ', - dev_effect_stack.slice(-10).map((d) => d.fn) - ); - dev_effect_stack = []; + } + // Try and handle the error so it can be caught at a boundary, that's + // if there's an effect available from when it was last scheduled + if (last_scheduled_effect !== null) { + if (DEV) { + try { + handle_error(error, last_scheduled_effect, null, null); + } catch (e) { + // Only log the effect stack if the error is re-thrown + log_effect_stack(); + throw e; + } + } else { + handle_error(error, last_scheduled_effect, null, null); + } + } else { + if (DEV) { + log_effect_stack(); + } throw error; } - } else { - e.effect_update_depth_exceeded(); } } flush_count++; @@ -637,8 +660,10 @@ function process_deferred() { const previous_queued_root_effects = queued_root_effects; queued_root_effects = []; flush_queued_root_effects(previous_queued_root_effects); + if (!is_micro_task_queued) { flush_count = 0; + last_scheduled_effect = null; if (DEV) { dev_effect_stack = []; } @@ -657,6 +682,8 @@ export function schedule_effect(signal) { } } + last_scheduled_effect = signal; + var effect = signal; while (effect.parent !== null) { @@ -776,6 +803,7 @@ export function flush_sync(fn) { } flush_count = 0; + last_scheduled_effect = null; if (DEV) { dev_effect_stack = []; } diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-20/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/Child.svelte new file mode 100644 index 0000000000..ff97dd96f0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/Child.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js new file mode 100644 index 0000000000..ccff614ade --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + let btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `
An error occurred!
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-20/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/main.svelte new file mode 100644 index 0000000000..18f216147f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/main.svelte @@ -0,0 +1,10 @@ + + + + + {#snippet failed()} +
An error occurred!
+ {/snippet} +