mirror of https://github.com/sveltejs/svelte
feat: add error boundaries (#14211)
* feat: add error boundary support tweak tweak again retry -> reset tweaks add tests tweaks tweaks tweaks more tests more tests and tweaks comments tweak tweak tweak tweak tweak * tweak tweak tweak tweak more fixes tweak tweak more fixes changeset * Update packages/svelte/elements.d.ts Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * Update .changeset/polite-peaches-do.md Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * fix issue with rethrowing * handle fallback error * handle fallback error * add more test coverage * more tests * more bug fixes * guard against non-errors * add component_stack to error * alternative approach * remove spread support * lint * add to legacy ast * add to svelte-html * disallow anything but attributes on the boundary element * fix error * more validation * only create block when necessary * swap argument order - results in nicer-looking code in many cases * Update .changeset/polite-peaches-do.md * simplify a bit * simplify * move declaration closer to usage * push once * unused * tweaks * consistent naming * simplify * add a couple newlines * tweak comments * simplify * newlines * placeholder documentation * add some docs * Update packages/svelte/src/internal/client/dom/blocks/boundary.js Co-authored-by: Rich Harris <rich.harris@vercel.com> * Update packages/svelte/src/internal/client/dom/blocks/boundary.js Co-authored-by: Rich Harris <rich.harris@vercel.com> * Update packages/svelte/src/internal/client/dom/blocks/boundary.js Co-authored-by: Rich Harris <rich.harris@vercel.com> * fix type * fix link * explain what happens if onerror throws --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com> Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/14486/head
parent
f2eed15c02
commit
ed7ebcde1e
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': minor
|
||||
---
|
||||
|
||||
feat: add error boundaries with `<svelte:boundary>`
|
@ -0,0 +1,79 @@
|
||||
---
|
||||
title: <svelte:boundary>
|
||||
---
|
||||
|
||||
```svelte
|
||||
<svelte:boundary onerror={handler}>...</svelte:boundary>
|
||||
```
|
||||
|
||||
Boundaries allow you to guard against errors in part of your app from breaking the app as a whole, and to recover from those errors.
|
||||
|
||||
If an error occurs while rendering or updating the children of a `<svelte:boundary>`, or running any [`$effect`]($effect) functions contained therein, the contents will be removed.
|
||||
|
||||
Errors occurring outside the rendering process (for example, in event handlers) are _not_ caught by error boundaries.
|
||||
|
||||
## Properties
|
||||
|
||||
For the boundary to do anything, one or both of `failed` and `onerror` must be provided.
|
||||
|
||||
### `failed`
|
||||
|
||||
If an `failed` snippet is provided, it will be rendered with the error that was thrown, and a `reset` function that recreates the contents ([demo](/playground/hello-world#H4sIAAAAAAAAE3VRy26DMBD8lS2tFCIh6JkAUlWp39Cq9EBg06CAbdlLArL87zWGKk8ORnhmd3ZnrD1WtOjFXqKO2BDGW96xqpBD5gXerm5QefG39mgQY9EIWHxueRMinLosti0UPsJLzggZKTeilLWgLGc51a3gkuCjKQ7DO7cXZotgJ3kLqzC6hmex1SZnSXTWYHcrj8LJjWTk0PHoZ8VqIdCOKayPykcpuQxAokJaG1dGybYj4gw4K5u6PKTasSbjXKgnIDlA8VvUdo-pzonraBY2bsH7HAl78mKSHZpgIcuHjq9jXSpZSLixRlveKYQUXhQVhL6GPobXAAb7BbNeyvNUs4qfRg3OnELLj5hqH9eQZqCnoBwR9lYcQxuVXeBzc8kMF8yXY4yNJ5oGiUzP_aaf_waTRGJib5_Ad3P_vbCuaYxzeNpbU0eUMPAOKh7Yw1YErgtoXyuYlPLzc10_xo_5A91zkQL_AgAA)):
|
||||
|
||||
```svelte
|
||||
<svelte:boundary>
|
||||
<FlakyComponent />
|
||||
|
||||
{#snippet failed(error, reset)}
|
||||
<button onclick={reset}>oops! try again</button>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> As with [snippets passed to components](snippet#Passing-snippets-to-components), the `failed` snippet can be passed explicitly as a property...
|
||||
>
|
||||
> ```svelte
|
||||
> <svelte:boundary {failed}>...</svelte:boundary>
|
||||
> ```
|
||||
>
|
||||
> ...or implicitly by declaring it directly inside the boundary, as in the example above.
|
||||
|
||||
### `onerror`
|
||||
|
||||
If an `onerror` function is provided, it will be called with the same two `error` and `reset` arguments. This is useful for tracking the error with an error reporting service...
|
||||
|
||||
```svelte
|
||||
<svelte:boundary onerror={(e) => report(e)}>
|
||||
...
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
...or using `error` and `reset` outside the boundary itself:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let error = $state(null);
|
||||
let reset = $state(() => {});
|
||||
|
||||
function onerror(e, r) {
|
||||
error = e;
|
||||
reset = r;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:boundary {onerror}>
|
||||
<FlakyComponent />
|
||||
</svelte:boundary>
|
||||
|
||||
{#if error}
|
||||
<button onclick={() => {
|
||||
error = null;
|
||||
reset();
|
||||
}}>
|
||||
oops! try again
|
||||
</button>
|
||||
{/if}
|
||||
```
|
||||
|
||||
If an error occurs inside the `onerror` function (or if you rethrow the error), it will be handled by a parent boundary if such exists.
|
@ -0,0 +1,27 @@
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { Context } from '../types' */
|
||||
import * as e from '../../../errors.js';
|
||||
|
||||
const valid = ['onerror', 'failed'];
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteBoundary} node
|
||||
* @param {Context} context
|
||||
*/
|
||||
export function SvelteBoundary(node, context) {
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type !== 'Attribute' || !valid.includes(attribute.name)) {
|
||||
e.svelte_boundary_invalid_attribute(attribute);
|
||||
}
|
||||
|
||||
if (
|
||||
attribute.value === true ||
|
||||
(Array.isArray(attribute.value) &&
|
||||
(attribute.value.length !== 1 || attribute.value[0].type !== 'ExpressionTag'))
|
||||
) {
|
||||
e.svelte_boundary_invalid_attribute_value(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/** @import { BlockStatement, Statement, Expression } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types' */
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteBoundary} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function SvelteBoundary(node, context) {
|
||||
const props = b.object([]);
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type !== 'Attribute' || attribute.value === true) {
|
||||
// these can't exist, because they would have caused validation
|
||||
// to fail, but typescript doesn't know that
|
||||
continue;
|
||||
}
|
||||
|
||||
const chunk = Array.isArray(attribute.value)
|
||||
? /** @type {AST.ExpressionTag} */ (attribute.value[0])
|
||||
: attribute.value;
|
||||
|
||||
const expression = /** @type {Expression} */ (context.visit(chunk.expression, context.state));
|
||||
|
||||
if (attribute.metadata.expression.has_state) {
|
||||
props.properties.push(b.get(attribute.name, [b.return(expression)]));
|
||||
} else {
|
||||
props.properties.push(b.init(attribute.name, expression));
|
||||
}
|
||||
}
|
||||
|
||||
const nodes = [];
|
||||
|
||||
/** @type {Statement[]} */
|
||||
const snippet_statements = [];
|
||||
|
||||
// Capture the `failed` implicit snippet prop
|
||||
for (const child of node.fragment.nodes) {
|
||||
if (child.type === 'SnippetBlock' && child.expression.name === 'failed') {
|
||||
/** @type {Statement[]} */
|
||||
const init = [];
|
||||
context.visit(child, { ...context.state, init });
|
||||
props.properties.push(b.prop('init', child.expression, child.expression));
|
||||
snippet_statements.push(...init);
|
||||
} else {
|
||||
nodes.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes }));
|
||||
|
||||
const boundary = b.stmt(
|
||||
b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block))
|
||||
);
|
||||
|
||||
context.state.template.push('<!>');
|
||||
context.state.init.push(
|
||||
snippet_statements.length > 0 ? b.block([...snippet_statements, boundary]) : boundary
|
||||
);
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
/** @import { BlockStatement } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types' */
|
||||
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
|
||||
/**
|
||||
* @param {AST.SvelteBoundary} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function SvelteBoundary(node, context) {
|
||||
context.state.template.push(
|
||||
b.literal(BLOCK_OPEN),
|
||||
/** @type {BlockStatement} */ (context.visit(node.fragment)),
|
||||
b.literal(BLOCK_CLOSE)
|
||||
);
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
/** @import { Effect, TemplateNode, } from '#client' */
|
||||
|
||||
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js';
|
||||
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
|
||||
import {
|
||||
active_effect,
|
||||
active_reaction,
|
||||
component_context,
|
||||
handle_error,
|
||||
set_active_effect,
|
||||
set_active_reaction,
|
||||
set_component_context,
|
||||
reset_is_throwing_error
|
||||
} from '../../runtime.js';
|
||||
import {
|
||||
hydrate_next,
|
||||
hydrate_node,
|
||||
hydrating,
|
||||
next,
|
||||
remove_nodes,
|
||||
set_hydrate_node
|
||||
} from '../hydration.js';
|
||||
import { queue_micro_task } from '../task.js';
|
||||
|
||||
/**
|
||||
* @param {Effect} boundary
|
||||
* @param {() => void} fn
|
||||
*/
|
||||
function with_boundary(boundary, fn) {
|
||||
var previous_effect = active_effect;
|
||||
var previous_reaction = active_reaction;
|
||||
var previous_ctx = component_context;
|
||||
|
||||
set_active_effect(boundary);
|
||||
set_active_reaction(boundary);
|
||||
set_component_context(boundary.ctx);
|
||||
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
set_active_effect(previous_effect);
|
||||
set_active_reaction(previous_reaction);
|
||||
set_component_context(previous_ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TemplateNode} node
|
||||
* @param {{
|
||||
* onerror?: (error: unknown, reset: () => void) => void,
|
||||
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
|
||||
* }} props
|
||||
* @param {((anchor: Node) => void)} boundary_fn
|
||||
* @returns {void}
|
||||
*/
|
||||
export function boundary(node, props, boundary_fn) {
|
||||
var anchor = node;
|
||||
|
||||
/** @type {Effect} */
|
||||
var boundary_effect;
|
||||
|
||||
block(() => {
|
||||
var boundary = /** @type {Effect} */ (active_effect);
|
||||
var hydrate_open = hydrate_node;
|
||||
var is_creating_fallback = false;
|
||||
|
||||
// We re-use the effect's fn property to avoid allocation of an additional field
|
||||
boundary.fn = (/** @type {unknown}} */ error) => {
|
||||
var onerror = props.onerror;
|
||||
let failed = props.failed;
|
||||
|
||||
// If we have nothing to capture the error, or if we hit an error while
|
||||
// rendering the fallback, re-throw for another boundary to handle
|
||||
if ((!onerror && !failed) || is_creating_fallback) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var reset = () => {
|
||||
pause_effect(boundary_effect);
|
||||
|
||||
with_boundary(boundary, () => {
|
||||
is_creating_fallback = false;
|
||||
boundary_effect = branch(() => boundary_fn(anchor));
|
||||
reset_is_throwing_error();
|
||||
});
|
||||
};
|
||||
|
||||
onerror?.(error, reset);
|
||||
|
||||
if (boundary_effect) {
|
||||
destroy_effect(boundary_effect);
|
||||
} else if (hydrating) {
|
||||
set_hydrate_node(hydrate_open);
|
||||
next();
|
||||
set_hydrate_node(remove_nodes());
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
// Render the `failed` snippet in a microtask
|
||||
queue_micro_task(() => {
|
||||
with_boundary(boundary, () => {
|
||||
is_creating_fallback = true;
|
||||
|
||||
try {
|
||||
boundary_effect = branch(() => {
|
||||
failed(
|
||||
anchor,
|
||||
() => error,
|
||||
() => reset
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
handle_error(error, boundary, null, boundary.ctx);
|
||||
}
|
||||
|
||||
reset_is_throwing_error();
|
||||
is_creating_fallback = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (hydrating) {
|
||||
hydrate_next();
|
||||
}
|
||||
|
||||
boundary_effect = branch(() => boundary_fn(anchor));
|
||||
reset_is_throwing_error();
|
||||
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT);
|
||||
|
||||
if (hydrating) {
|
||||
anchor = hydrate_node;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
const [btn1, btn2] = target.querySelectorAll('button');
|
||||
|
||||
btn2?.click();
|
||||
btn1?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught!!!']);
|
||||
}
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
<script>
|
||||
function throw_error() {
|
||||
throw new Error('test')
|
||||
}
|
||||
|
||||
let count = $state(0);
|
||||
let onerror = $state((e) => console.log('error caught'));
|
||||
</script>
|
||||
|
||||
<svelte:boundary {onerror}>
|
||||
{count > 0 ? throw_error() : null}
|
||||
</svelte:boundary>
|
||||
|
||||
<button onclick={() => count++}>+</button>
|
||||
<button onclick={() => onerror = () => console.log('error caught!!!')}>change error message</button>
|
||||
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
const [btn1, btn2] = target.querySelectorAll('button');
|
||||
|
||||
btn2?.click();
|
||||
btn1?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught!!!']);
|
||||
}
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
<script>
|
||||
function throw_error() {
|
||||
throw new Error('test')
|
||||
}
|
||||
|
||||
let count = $state(0);
|
||||
let props = $state({ onerror: (e) => console.log('error caught') });
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={props.onerror}>
|
||||
{count > 0 ? throw_error() : null}
|
||||
</svelte:boundary>
|
||||
|
||||
<button onclick={() => count++}>+</button>
|
||||
<button onclick={() => props = { onerror: () => console.log('error caught!!!') }}>change error message</button>
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target }) {
|
||||
const btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `<button>change</button><p>Error occured</p>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
const things = $derived.by(() => {
|
||||
if (count === 1) {
|
||||
throw new Error('123')
|
||||
}
|
||||
return [1, 2 ,3]
|
||||
})
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>change</button>
|
||||
|
||||
<svelte:boundary>
|
||||
{#each things as thing}
|
||||
<p>{thing}</p>
|
||||
{/each}
|
||||
|
||||
{#snippet failed()}
|
||||
<p>Error occured</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
const { things } = $props();
|
||||
|
||||
$effect(() => {
|
||||
things
|
||||
})
|
||||
</script>
|
@ -0,0 +1,13 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target }) {
|
||||
const btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `<button>change</button><p>Error occured</p>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
|
||||
let count = $state(0);
|
||||
|
||||
const things = $derived.by(() => {
|
||||
if (count === 1) {
|
||||
throw new Error('123')
|
||||
}
|
||||
return [1, 2 ,3]
|
||||
})
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>change</button>
|
||||
|
||||
<svelte:boundary>
|
||||
<Child {things} />
|
||||
|
||||
{#snippet failed()}
|
||||
<p>Error occured</p>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
function throw_error() {
|
||||
throw new Error('throw_error');
|
||||
}
|
||||
</script>
|
||||
|
||||
{throw_error()}
|
||||
|
||||
<div>Foo</div>
|
@ -0,0 +1,9 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
|
||||
test({ assert, logs }) {
|
||||
assert.deepEqual(logs, ['error caught']);
|
||||
}
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
import Child from "./Child.svelte";
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught')}>
|
||||
<Child />
|
||||
</svelte:boundary>
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
function throw_error() {
|
||||
throw new Error('throw_error');
|
||||
}
|
||||
</script>
|
||||
|
||||
{throw_error()}
|
||||
|
||||
<div>Foo</div>
|
@ -0,0 +1,9 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
|
||||
test({ assert, logs }) {
|
||||
assert.deepEqual(logs, ['error caught 1', 'error caught 2']);
|
||||
}
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
<script>
|
||||
import Child from "./Child.svelte";
|
||||
|
||||
function throw_error() {
|
||||
throw new Error('test')
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught 2')}>
|
||||
<svelte:boundary onerror={(e) => console.log('error caught 1')}>
|
||||
<Child />
|
||||
|
||||
{#snippet failed()}
|
||||
{throw_error()}
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
</svelte:boundary>
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
$effect.pre(() => {
|
||||
if (count > 1) {
|
||||
throw new Error('too high');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{count}
|
||||
|
||||
<button onclick={() => count++}>+</button>
|
@ -0,0 +1,24 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
let btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught']);
|
||||
assert.htmlEqual(target.innerHTML, `<div>An error occurred!</div>\n0\n<button>+</button>`);
|
||||
|
||||
btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught', 'error caught']);
|
||||
assert.htmlEqual(target.innerHTML, `<div>An error occurred!</div>\n0\n<button>+</button>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught')}>
|
||||
<Child />
|
||||
|
||||
{#snippet failed(err, reset)}
|
||||
<div>An error occurred!</div>
|
||||
<Child />
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
const { initial = 0 } = $props();
|
||||
|
||||
let count = $state(initial);
|
||||
|
||||
$effect.pre(() => {
|
||||
if (count > 1) {
|
||||
throw 'too high';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{count}
|
||||
|
||||
<button onclick={() => count++}>+</button>
|
@ -0,0 +1,15 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
let btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught 1', 'error caught 2']);
|
||||
assert.htmlEqual(target.innerHTML, `<div>content before</div><div>content after</div>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
</script>
|
||||
|
||||
<div>content before</div>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught 2')}>
|
||||
<svelte:boundary onerror={(e) => console.log('error caught 1')}>
|
||||
<Child />
|
||||
|
||||
{#snippet failed(err, reset)}
|
||||
<div>An error occurred! {err}</div>
|
||||
<Child initial={2} />
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
</svelte:boundary>
|
||||
|
||||
<div>content after</div>
|
@ -0,0 +1,15 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target }) {
|
||||
let btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
btn?.click();
|
||||
|
||||
assert.throws(() => {
|
||||
flushSync();
|
||||
}, /test\n\n\tin {expression}\n/);
|
||||
}
|
||||
});
|
@ -0,0 +1,16 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
let test = $derived.by(() => {
|
||||
if (count > 1) {
|
||||
throw new Error('test');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => { throw(e) }}>
|
||||
<div>Count: {count}</div>
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
{count} / {test}
|
||||
</svelte:boundary>
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
let btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught 1', 'error caught 2']);
|
||||
}
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
let test = $derived.by(() => {
|
||||
if (count > 1) {
|
||||
throw new Error('test');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => {console.log('error caught 1')}}>
|
||||
<div>Count: {count}</div>
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
{count} / {test}
|
||||
</svelte:boundary>
|
||||
|
||||
|
||||
<svelte:boundary onerror={(e) => {console.log('error caught 2')}}>
|
||||
<div>Count: {count}</div>
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
{count} / {test}
|
||||
</svelte:boundary>
|
@ -0,0 +1,13 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
const btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught']);
|
||||
}
|
||||
});
|
@ -0,0 +1,15 @@
|
||||
<script>
|
||||
function throw_error() {
|
||||
throw new Error('test')
|
||||
}
|
||||
|
||||
let count = $state(0);
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught')}>
|
||||
{count > 0 ? throw_error() : null}
|
||||
</svelte:boundary>
|
||||
|
||||
<button onclick={() => count++}>+</button>
|
||||
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
const btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught']);
|
||||
assert.htmlEqual(target.innerHTML, `<div>Fallback!</div><button>+</button>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
<script>
|
||||
function throw_error() {
|
||||
throw new Error('test')
|
||||
}
|
||||
|
||||
let count = $state(0);
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught')}>
|
||||
{count > 0 ? throw_error() : null}
|
||||
|
||||
{#snippet failed()}
|
||||
<div>Fallback!</div>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
|
||||
<button onclick={() => count++}>+</button>
|
@ -0,0 +1,14 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
const btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught']);
|
||||
assert.htmlEqual(target.innerHTML, `<div>Fallback!</div><button>+</button>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
<script>
|
||||
function throw_error() {
|
||||
throw new Error('test')
|
||||
}
|
||||
|
||||
let count = $state(0);
|
||||
</script>
|
||||
|
||||
{#snippet failed()}
|
||||
<div>Fallback!</div>
|
||||
{/snippet}
|
||||
|
||||
<svelte:boundary {failed} onerror={(e) => console.log('error caught')}>
|
||||
{count > 0 ? throw_error() : null}
|
||||
</svelte:boundary>
|
||||
|
||||
<button onclick={() => count++}>+</button>
|
@ -0,0 +1,13 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
$effect.pre(() => {
|
||||
if (count > 1) {
|
||||
throw new Error('too high');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{count}
|
||||
|
||||
<button onclick={() => count++}>+</button>
|
@ -0,0 +1,31 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
let btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught']);
|
||||
assert.htmlEqual(target.innerHTML, `<div>too high</div><button>Retry</button>`);
|
||||
|
||||
const [btn2] = target.querySelectorAll('button');
|
||||
|
||||
btn2?.click();
|
||||
flushSync();
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `0\n<button>+</button>`);
|
||||
|
||||
btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.deepEqual(logs, ['error caught', 'error caught']);
|
||||
assert.htmlEqual(target.innerHTML, `<div>too high</div><button>Retry</button>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught')}>
|
||||
<svelte:boundary onerror={(e) => { throw e }}>
|
||||
<svelte:boundary>
|
||||
<Child />
|
||||
</svelte:boundary>
|
||||
</svelte:boundary>
|
||||
|
||||
{#snippet failed(e, retry)}
|
||||
<div>too high</div>
|
||||
<button onclick={retry}>Retry</button>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target }) {
|
||||
const btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
flushSync();
|
||||
|
||||
assert.htmlEqual(target.innerHTML, `<button>+</button><div>There is an error!</div>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
function throw_error() {
|
||||
throw new Error('test')
|
||||
}
|
||||
|
||||
let count = $state(0);
|
||||
let error = $state();
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => error = e}>
|
||||
{count > 0 ? throw_error() : null}
|
||||
</svelte:boundary>
|
||||
|
||||
<button onclick={() => count++}>+</button>
|
||||
|
||||
{#if error}
|
||||
<div>There is an error!</div>
|
||||
{/if}
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
$effect.pre(() => {
|
||||
throw new Error('oh noes');
|
||||
});
|
||||
</script>
|
@ -0,0 +1,8 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
assert.deepEqual(logs, ['error caught']);
|
||||
assert.htmlEqual(target.innerHTML, `<div>Error!</div><button>Retry</button>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught')}>
|
||||
<Child />
|
||||
|
||||
{#snippet failed(e, retry)}
|
||||
<div>Error!</div>
|
||||
<button onclick={retry}>Retry</button>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
$effect.pre(() => {
|
||||
throw new Error('oh noes');
|
||||
});
|
||||
</script>
|
@ -0,0 +1,8 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
assert.deepEqual(logs, ['error caught']);
|
||||
assert.htmlEqual(target.innerHTML, `<div>Error!</div><button>Retry</button>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught')}>
|
||||
<!-- boundary should rethrow error as there's no control flow -->
|
||||
<svelte:boundary>
|
||||
<Child />
|
||||
</svelte:boundary>
|
||||
|
||||
{#snippet failed(e, retry)}
|
||||
<div>Error!</div>
|
||||
<button onclick={retry}>Retry</button>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
$effect.pre(() => {
|
||||
throw new Error('oh noes');
|
||||
});
|
||||
</script>
|
@ -0,0 +1,8 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
test({ assert, target, logs }) {
|
||||
assert.deepEqual(logs, ['error caught']);
|
||||
assert.htmlEqual(target.innerHTML, `<div>Error!</div><button>Retry</button>`);
|
||||
}
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import Child from './Child.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught')}>
|
||||
<!-- boundary should rethrow error as there's no control flow -->
|
||||
<svelte:boundary>
|
||||
<!-- rethrow the error in the handler -->
|
||||
<svelte:boundary onerror={e => { throw e }}>
|
||||
<Child />
|
||||
</svelte:boundary>
|
||||
</svelte:boundary>
|
||||
|
||||
{#snippet failed(e, retry)}
|
||||
<div>Error!</div>
|
||||
<button onclick={retry}>Retry</button>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['client'],
|
||||
|
||||
test({ assert, logs }) {
|
||||
assert.deepEqual(logs, ['error caught']);
|
||||
}
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
<script>
|
||||
function throw_error() {
|
||||
throw new Error('test')
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:boundary onerror={(e) => console.log('error caught')}>
|
||||
{throw_error()}
|
||||
</svelte:boundary>
|
||||
|
||||
|
Loading…
Reference in new issue