kit icon indicating copy to clipboard operation
kit copied to clipboard

Svelte boundary is not catching an awaited remote function error

Open iatrophobic opened this issue 2 months ago • 9 comments

Describe the bug

According to the docs if an error is thrown inside of a remote function it should be handled by the nearest svelte:boundary. However, when an error is thrown the svelte error page is displayed instead.

If you navigate to the error it will display the error occasionally on first load, however if the user reloads the page for any reason the error page is displayed.

Reproduction

Stackblitz

Logs

No logs are output

System Info

System:
    OS: macOS 26.0
    CPU: (10) arm64 Apple M4
    Memory: 158.17 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.13.1 - /Users/agill/.nvm/versions/node/v22.13.1/bin/node
    npm: 11.2.0 - /Users/agill/.nvm/versions/node/v22.13.1/bin/npm
    pnpm: 10.18.1 - /Users/agill/.nvm/versions/node/v22.13.1/bin/pnpm
    bun: 1.2.21 - /Users/agill/.bun/bin/bun
  Browsers:
    Chrome: 141.0.7390.123
    Safari: 26.0
  npmPackages:
    @sveltejs/adapter-auto: ^6.1.0 => 6.1.1 
    @sveltejs/kit: ^2.43.2 => 2.47.3 
    @sveltejs/vite-plugin-svelte: ^6.2.0 => 6.2.1 
    svelte: ^5.39.5 => 5.41.3 
    vite: ^7.1.7 => 7.1.12

Severity

annoyance

Additional Information

No response

iatrophobic avatar Oct 25 '25 20:10 iatrophobic

After digging into this more, it looks like you have to have both a pending and failed snippet present inside of the boundary. I only had a failed snippet. Once I added in a pending snippet, it works as expected.

This is contradicted in the docs which says that only one or more of the following are needed.

  • pending
  • failed
  • onerror

I'm not sure if its a documentation error or an error with how it's supposed to work, or if I am just not understanding how boundaries handle awaited remote functions with the appropriate experimental flags enabled.

iatrophobic avatar Oct 27 '25 13:10 iatrophobic

Could it be that the error is thrown during SSR? If so, I don’t believe boundaries are able to catch those (yet(?)) The pending snippet turns off SSR for that function, as the pending snippet is server rendered instead. So it becomes a client side error again, which can be caught by the boundary

MotionlessTrain avatar Oct 27 '25 13:10 MotionlessTrain

Could it be that the error is thrown during SSR? If so, I don’t believe boundaries are able to catch those (yet(?)) The pending snippet turns off SSR for that function, as the pending snippet is server rendered instead. So it becomes a client side error again, which can be caught by the boundary

unless i am misunderstanding the docs here

Image

the boundary should be able to catch it as long as the error occurs inside of the promise and not after the promise work has completed according to.

Image

iatrophobic avatar Oct 27 '25 13:10 iatrophobic

This works as expected from what I can tell. If the error happens during SSR the error boundaries are ignored.

We have not documented this though, and we should.

Aside from that I cannot reproduce the "sometimes it doesn't work when navigating on the client either" - for me it always shows the correct boundary then.

dummdidumm avatar Oct 27 '25 19:10 dummdidumm

@dummdidumm I have updated the stackblitz repro to include a pending snippet, which then allows the page to be displayed. Admittedly i do not know enough to understand why that catches the error now and displays the failed snippet. If you remove the pending snippet, it goes to the error page instead of just displaying the error snippet immediately. I was just caught up in the docs where it says one is all you need. I'm guessing this has to do with the timing of when the async call actually happens when there is a pending snippet available vs not?

iatrophobic avatar Oct 27 '25 20:10 iatrophobic

I've been battling this behavior for the past two days.

I'm using <svelte:boundary> to catch an error from my Events component, like this:

	<svelte:boundary>
		<Events headline="Die nächsten 6 Termine" limit={6} />
		{#snippet failed(error)}
			{error}
		{/snippet}
	</svelte:boundary>

For testing, my remote function is set to always throw an error:

Image

I would expect the #snippet failed(error) to catch this and render the error. However, the whole page crashes instead:

Image

It seems the failed snippet only works if I also provide a #snippet pending. This code works:

Image

This feels a bit weird. Is it the intended behavior that a pending snippet is required for the failed snippet to function correctly?

In my case, I intentionally omitted the pending snippet because I don't want extra loading markup, as it causes a flash of text.

garytube avatar Oct 27 '25 22:10 garytube

@dummdidumm I have updated the stackblitz repro to include a pending snippet, which then allows the page to be displayed. Admittedly i do not know enough to understand why that catches the error now and displays the failed snippet. If you remove the pending snippet, it goes to the error page instead of just displaying the error snippet immediately. I was just caught up in the docs where it says one is all you need. I'm guessing this has to do with the timing of when the async call actually happens when there is a pending snippet available vs not?

Because the pending snippet prevents SSR of the async operation. The pending snippet is rendered on the server instead. And the async operation then happens in the client, allowing the boundary to catch the error.

sillvva avatar Oct 29 '25 02:10 sillvva

@dummdidumm I have updated the stackblitz repro to include a pending snippet, which then allows the page to be displayed. Admittedly i do not know enough to understand why that catches the error now and displays the failed snippet. If you remove the pending snippet, it goes to the error page instead of just displaying the error snippet immediately. I was just caught up in the docs where it says one is all you need. I'm guessing this has to do with the timing of when the async call actually happens when there is a pending snippet available vs not?

Because the pending snippet prevents SSR of the async operation. The pending snippet is rendered on the server instead. And the async operation then happens in the client, allowing the boundary to catch the error.

Ah gotcha...ok great thanks. Learned something new.

iatrophobic avatar Oct 29 '25 02:10 iatrophobic

This is definitely a bug. Errors thrown inside remote functions are only caught properly on the client side. SvelteKit 2.49.0, Svelte 5.44.0

+layout.svelte

<script lang="ts">
	let { children } = $props()
</script>

<svelte:boundary>
	{@render children()}

	{#snippet failed(error, reset)}
		<button onclick={reset}>oops! try again</button>
	{/snippet}
</svelte:boundary>

+page.svelte

<script lang="ts">
    import { getItems } from './data.remote.ts'
    const drops = $derived(await getItems())
</script>

+data.remote.ts

import { query } from '$app/server'
export const getItems = query(async()=>{
    throw new Error('test')
})

Although the SvelteKit documentation states that such errors should be handled using svelte:boundary, in practice SSR errors from remote functions crash the entire app and bypass any boundary and +error.svelte pages. The only place they appear is in handleError within hooks.server.ts.

exentrich avatar Nov 25 '25 16:11 exentrich

If I understand correctly, to get a stable boundary (or query.error) with ssr there are two options (by design):

  • use the pending snippet that "forces" the async operation to be moved to the client side
  • never throw or use error() in a query, but return errors as normal payload.

In other cases (apart from the above bug), it may happen that at page load the +error page could be triggered instead of the boundary, is it ? If so, it may reduce DX and create surprises.

dil-zgaal avatar Dec 19 '25 09:12 dil-zgaal