Integrations
SvelteKit
SvelteKit is not a rendering library with a router bolted on, and that is why it
has an error story the page above cannot tell: it owns the
error. sveltekit error handling is seven suggestions on Google, more than
twice svelte error handling's three, and the two the autocomplete adds that
the others do not have are sveltekit global error handling and sveltekit remote functions error handling. That is a framework with hooks of its own, and
therefore a page of its own.
Everything below was read out of @sveltejs/[email protected], the published package,
not out of the documentation. The documentation is good, and it describes
handleError as the place to log an error. The first thing below is the
opposite of what adding one does, and it is not written down anywhere.
The correction to the obvious plan, first, because it is the whole page: the
hook that files a report is the one in src/hooks.ts, not the one in
src/hooks.server.ts. A server hook runs in Node, where there is no window, no
ring buffer and no panel to open — a report sent from it describes a machine
your user is not looking at. The universal hook runs in the browser, on
client-side navigation, which is where the panel is. You want both, for different
reasons, and the second half of this page is what each one is for.
The hook that decides whether you have any reports at all #
A SvelteKit application with no src/hooks.ts still logs its client-side
errors. The client runtime's hooks are generated into the manifest, and the
generated line is this — src/core/sync/write_client_manifest.js in the kit
package:
export const hooks = {
handleError: ${client_hooks_file ? 'client_hooks.handleError || ' : ''}(({ error }) => { console.error(error) }),
${client_hooks_file ? 'init: client_hooks.init,' : ''}
reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {}),
transport: ${universal_hooks_file ? 'universal_hooks.transport || ' : ''}{}
};Read the middle of that line twice. It is your hook, or a function that logs.
Adding a handleError to src/hooks.ts replaces the logging function with
yours — so if your hook posts a report and does not itself console.error, the
console line disappears at the exact moment you wire the integration up. A
developer testing the integration sees a working report; the person triaging
later finds an empty inbox for a bug the browser used to print for free.
The ?? on the far side of the call is the second half of it. In
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 })
);
}What the hook returns is the object +error.svelte renders, and the ?? is
what makes returning nothing legal: the page then gets { message } and nothing
else — not the status code, not the stack. So the hook's return value has two
consumers and they want different things, and the mistake is a hook that both
reports and returns. Send the report and return nothing, or return something and
do not report. The report is the reason the hook exists; the returned value is a
page.
// src/hooks.ts
import type { HandleClientError } from "@sveltejs/kit";
import { buildReport, sendReport } from "bugbottle";
export const handleError: HandleClientError = ({ error, message, event }) => {
void sendReport(buildReport({ type: "error", message, url: event.url.pathname }));
// Return nothing: the page renders its own message from the fallback above.
};src/hooks.ts is a universal hook, and that distinction is the point of the
paragraph. It runs in the browser on client-side navigation and in Node on the
server, out of the same file, and a console.error in it reaches the ring buffer
in the first of those two only.
The server hook logs where nobody is listening #
The server's default is a different function, and it is different on purpose —
src/runtime/server/index.js:
handleError:
module.handleError ||
(({ status, error, event }) => {
const error_message = format_server_error(
status,
/** @type {Error} */ (error),
event
);
console.error(error_message);
}),format_server_error is SvelteKit's own formatting, and that console.error is
Node's. An error thrown while server-rendering is printed in the server log and
nowhere else: no window, no ring buffer, no panel. Which is the correct place
for it — a server log is where an SSR failure belongs — and is why
src/hooks.server.ts is not where a bugbottle report comes from. Forward from
there to whatever you already run for server errors; report from
src/hooks.ts.
And a server error is deliberately not reported twice. When a load fails
on the server, the client gets the error as a __data.json response and turns
it into an HttpError on purpose — src/runtime/client/client.js, in
load_data:
if (!res.ok) {
// error message is a JSON-stringified string which devalue can't handle at the top level
// turn it into a HttpError to not call handleError on the client again (was already handled on the server)Read that comment as a specification: the client hook does not fire for an error that already happened on the server. So neither hook covers the app on its own:
| Where the error happened | src/hooks.ts |
src/hooks.server.ts |
|---|---|---|
| A component throws during client-side navigation | yes | no |
A universal load throws on the client |
yes | no |
A server load or a +page.server.js action throws |
no, on purpose | yes |
A form action calls fail() |
no | no |
The last row is not a gap in the table, it is the rule: fail() is a result,
not an error. It is a validation answer with a status code and a data object,
and SvelteKit never routes it through handleError. If your users report "the
form does nothing" and you have no reports, this row is why.
error() is not yours, and a server error arrives as a copy #
Two things from the Svelte page carry over unchanged, so they
are linked rather than repeated. error(404, "Not found") is an HttpError, so
handle_error returns at its first line and your hook never sees it. And a
server-rendered error handed to the client hook is a deserialised copy: the
Error crossed the wire, so instanceof against your own error classes fails
and the stack is whatever the server chose to send. Print it once from inside
the hook to see what your own report will carry.
The line that is missing everywhere else, and is the reason a SvelteKit report
that goes missing usually has nothing to do with the hook: a rejected promise
is not a SvelteKit error. unhandledrejection appears zero times in
@sveltejs/[email protected]. A fetch in a load that nobody awaits, or a remote
function call a component forgets to catch, reaches the window, and the ring
buffer is already patched for it. The hook will not see those, and does not need
to.
Remote functions: the same trap on a second hook #
sveltekit remote functions error handling is the second autocomplete
suggestion, and remote functions brought a second hook with the identical ||
shape. The default, again from src/runtime/server/index.js:
handleValidationError:
module.handleValidationError ||
(({ issues }) => {
console.error('Remote function schema validation failed:', issues);
return { message: 'Bad Request' };
}),A schema mismatch on a remote function's arguments is a 400 with a server-side
console.error: not an exception, not an HttpError you threw, and not
something handleError ever sees. It is your own argument validation failing
somewhere between the browser and the function, and by default the only trace is
a line in a server log. Give it a handleValidationError in
src/hooks.server.ts if you want those visible. The shape is the same choice as
before: what the hook returns is the response body, so returning a different
message is how you change what the browser is told.
The pages the panel is not on #
+error.svelte is a separate page load without the layout, so the panel is not
mounted on it, and src/error.html is the static file SvelteKit serves when the
app cannot boot — the same two answers as the other framework pages, and the
full snippet is on the Svelte page.
One note is SvelteKit's alone: src/error.html is served before any of your
code runs, so it is the one page where the tag has to be inline in the markup,
and it is also the page a reader lands on when the whole application is down.
Mount the panel there the same way, knowing that nothing else in your app is
available to it.
What to check before you ship it #
Four throws, and the reports that exist afterwards. First, throw inside a
component during a client-side navigation with no src/hooks.ts at all and
watch the console line appear — that is the generated default, and it is the
baseline. Second, add the hook from the first section and throw again: the
report arrives and the console line is gone. That silence is the hook doing its
job, and it is worth knowing before somebody reports it as a regression. Third,
build, serve the build, and throw inside a server-rendered page: the client hook
does not fire and the server log is the only trace — the table's second half,
measured rather than believed. Fourth, call a remote function with arguments
that fail its schema, and confirm that no report appears anywhere: a 400 is not
an error until somebody says it is.