Integrations
Svelte
A Svelte 5 application has one thing React and Vue do not have: an element you
put in the markup. <svelte:boundary> is a wall around a piece of the tree,
it takes an onerror, and since 5.3.0 that is the whole per-component answer.
What is left is four things Svelte's own <svelte:boundary> page does not say,
and two of them decide whether your report arrives with a stack in it. Every
claim below was read out of [email protected] and @sveltejs/[email protected], both
fetched from their npm tarballs.
The integration #
<!-- App.svelte, or the one component that wraps the app -->
<script>
import { mountBugbottle } from "bugbottle/ui";
import { htmlToImage } from "bugbottle/html-to-image"; // optional
import { da } from "bugbottle/locales";
const widget = mountBugbottle({
endpoint: "/api/feedback",
screenshot: htmlToImage,
locale: da,
openOnError: { prefill: true },
});
</script>
<svelte:boundary
onerror={(error) => {
widget.open();
// Keep the console. See below: a boundary is not a logger.
console.error(error);
}}
>
<Router />
</svelte:boundary><svelte:boundary> is not an error handler in the sense the other pages
describe. It replaces the content it wraps, which is the point of it, and it
reports what happened on the way. Both at once is the whole integration for a
client-rendered Svelte app; the store from bugbottle/svelte
is the alternative if you want your own markup instead of the panel.
A boundary with only a pending snippet is invisible #
This is the Svelte version of Vue's onErrorCaptured trap, and it has the
opposite shape. Vue's hook swallows the error; a Svelte boundary passes it
on. The runtime says so in the first three lines of its error method:
// src/internal/client/dom/blocks/boundary.js
error(error) {
// 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 (!this.#props.onerror && !this.#props.failed) {
throw error;
}So a boundary added for its loading state, with no onerror and no failed,
is not in the error path at all — the error goes straight past it to the
parent boundary, and if there is no parent it reaches the window, where the
console ring buffer still sees it but your handler never ran. The same comment
is the second half of the trap: an error thrown while rendering the failed
snippet is re-thrown too, so a fallback that reads a property of the error
that does not exist there throws again, and the second throw is not caught by
the boundary that just failed.
The error your onerror receives on a server-rendered page is a copy #
If the page was server-rendered and the server failed inside the boundary, the
client does not get the Error. It gets what the server serialised into a
hydration comment, and your handler is called with that:
#hydrate_failed_content(error) {
const failed = this.#props.failed;
const { reset, invoke_onerror } = this.#create_reset(error);
// `onerror` may mutate state, which is disallowed while hydrating
queue_micro_task(invoke_onerror);Svelte's own docs put it in one sentence: "If the boundary has an onerror
handler, it will be called upon hydration with the deserialized error object."
So on a server-rendered page the report carries whatever transformError
returned — usually a { message }, no stack, no name, and an Error-shaped
object that is not an Error. Two consequences worth designing for: a
console.error(error) in the handler prints the copy rather than the original,
and code that does error instanceof TypeError is false on the client and true
on the server for the same failure.
A boundary does nothing on the server, and reset is a no-op the second time #
Two smaller facts from the same file, both read rather than documented.
Nothing is caught server-side by default. Svelte's docs: "By default,
error boundaries have no effect on the server — if an error occurs during
rendering, the render as a whole will fail." Since 5.51 render() takes a
transformError, and the docs are explicit that a framework has to wire it up:
"SvelteKit will add support for this in the near future, via the handleError
hook." As of @sveltejs/[email protected] that is still in the future, so in a
SvelteKit app a render error is a 500 and your onerror is not involved.
reset is idempotent. #create_reset guards on did_reset and returns
after a dev-only svelte_boundary_reset_noop() warning, so a "try again"
button that fires twice re-renders once. That is worth knowing because the
second onerror call is a separate report: the server's dedupe on the report
fingerprint is the second half of the answer.
SvelteKit's handleError skips a deliberate error() #
handleError is the hook for a SvelteKit application — handleError in
src/hooks.server.ts for the server, the same name in src/hooks.ts for
client-side navigation. It is not called for every failure:
// src/runtime/client/client.js
function handle_error(error, event) {
if (error instanceof HttpError) {
return error.body;
}
// …
return (
app.hooks.handleError({ error, event, status, message }) ??
/** @type {any} */ ({ message })
);
}An error(404, "Not found") is an HttpError, so it returns at the first
line and your hook never sees it — which is correct, and the reason a SvelteKit
app must not treat handleError as a report-everything switch. What the hook
returns is also the shape +error.svelte renders, and when it returns nothing
that shape is { message }: the status code and the original stack are not in
it. Send the report from inside the hook and return nothing, rather than
returning a value you also want reported.
The other half of the rule from the other framework pages holds here too: the
client runtime's two console.error calls in its error paths are both inside
if (DEV) — one for a failing reroute hook, one for "An error occurred while
loading the page. This will cause a full page reload." — and handle_error
itself has none. So a client-side navigation error is logged by your hook or
by nobody.
A rejected promise is not a SvelteKit error. unhandledrejection appears
zero times in @sveltejs/[email protected]. A failed fetch in a load function
that nobody awaits reaches the window, and the ring buffer is already patched
for it.
The page where the framework is gone #
+error.svelte is a separate page load without the layout, so the panel is
not mounted on it, and src/error.html is a static file the framework serves
when the app cannot boot at all. Same answer as the other four frameworks: one
button, mounted in the error page itself.
<!-- src/routes/+error.svelte -->
<script>
import { mountBugbottle } from "bugbottle/ui";
const widget = mountBugbottle({ endpoint: "/api/feedback" });
</script>
<h1>Something went wrong.</h1>
<button onclick={() => widget.open()}>Report it</button>What to check before you ship it #
Five throws, and a report from the right ones. First, from a child component's
render: the panel opens and the report has a stack. Second, from a setTimeout
callback and third, from an onclick handler: no boundary sees either of
them — that is the documented rule, and the reports only exist if somebody
presses the trigger, which is the half the trigger is for. Fourth, add a
boundary with a pending snippet and no onerror around a component that
throws, and confirm the handler you wrote earlier does not run. Fifth, build
and serve the build, then throw inside a server-rendered page: the client
handler is called with a deserialised copy, and printing it is the only way to
see that. Throw from the failed snippet too, since the runtime re-throws that
one on purpose.