<Errored/>

Error boundary. Catches errors thrown by descendants and renders a fallback in their place. It protects its subtree from both synchronous throws during render and reactive throws inside descendant effect, memo and derived. The programmatic form is catchError.

Attributes

name type description
fallback? any | (err, reset) => JSX.Element what to render when a descendant throws. When the fallback is a function it receives the thrown value and a reset function that clears the error and re-runs the subtree.
children any the protected subtree

Notes

Examples

Catch a synchronous throw

Boom throws during render until ok is set. The function fallback receives the error and a reset; the retry button fixes the state, then calls reset() to re-render the protected subtree.

import { render, signal } from 'pota'
import { Errored } from 'pota/components'

const ok = signal(false)

function Boom() {
	if (!ok.read()) throw new Error('kaboom')
	return <p>Everything is fine now.</p>
}

render(
	<Errored
		fallback={(err, reset) => (
			<div>
				<p>Something broke: {String(err)}</p>
				<button
					on:click={() => {
						ok.write(true)
						reset()
					}}
				>
					retry
				</button>
			</div>
		)}
	>
		<Boom />
	</Errored>,
)

Boundary with reset

Catches the throw from a descendant and renders fallback; calling the reset passed to the function fallback clears the error and re-renders the children. The protected child is a function so it re-evaluates when show flips.

import { render, signal } from 'pota'
import { Errored } from 'pota/components'

function Risky() {
	throw new Error('something broke')
}

function App() {
	const show = signal(false)

	return (
		<div>
			<button on:click={() => show.update(s => !s)}>toggle</button>
			<Errored
				fallback={(err, reset) => (
					<div>
						<p>error: {String(err)}</p>
						<button on:click={reset}>retry</button>
					</div>
				)}
			>
				{() => (show.read() ? <Risky /> : <p>safe</p>)}
			</Errored>
		</div>
	)
}

render(App)

Different fallback shapes

fallback accepts these shapes: a JSX element for a static "something failed" view, a primitive (string/number) when you just want a placeholder, or a (err, reset) => JSX function when you want to show the error and let the user recover. The function form is the most common.

import { render, signal } from 'pota'
import { Errored, Match, Switch } from 'pota/components'

function Bomb() {
	throw new Error('kaboom')
}

function App() {
	const which = signal('static')

	return (
		<div>
			<button on:click={() => which.write('static')}>
				static jsx
			</button>
			<button on:click={() => which.write('value')}>
				plain value
			</button>
			<button on:click={() => which.write('function')}>
				function with reset
			</button>

			<Switch>
				<Match when={() => which.read() === 'static'}>
					<Errored fallback={<p>oh no, an error happened</p>}>
						<Bomb />
					</Errored>
				</Match>
				<Match when={() => which.read() === 'value'}>
					<Errored fallback="— ">
						<Bomb />
					</Errored>
				</Match>
				<Match when={() => which.read() === 'function'}>
					<Errored
						fallback={(err, reset) => (
							<div>
								<p>{String(err)}</p>
								<button on:click={reset}>retry</button>
							</div>
						)}
					>
						<Bomb />
					</Errored>
				</Match>
			</Switch>
		</div>
	)
}

render(App)

Async derived chain

<Errored/> also catches rejections from derived chains, action pipelines, and async work in effects. Here parse failures and HTTP errors land in the same fallback regardless of how they were thrown — handy for network-driven views.

import { derived, render, signal } from 'pota'
import { Errored } from 'pota/components'

function Catalog() {
	const id = signal(1)

	const post = derived(
		() => `https://jsonplaceholder.typicode.com/posts/${id.read()}`,
		url =>
			fetch(url).then(r => {
				if (!r.ok) throw new Error(`HTTP ${r.status}`)
				return r.json()
			}),
	)

	return (
		<div>
			<button on:click={() => id.update(n => n + 1)}>next</button>
			<button on:click={() => id.write(-1)}>force 404</button>
			<h2>{() => post()?.title ?? 'loading…'}</h2>
		</div>
	)
}

function App() {
	return (
		<Errored
			fallback={(err, reset) => (
				<div>
					<p>fetch failed: {String(err)}</p>
					<button on:click={reset}>try again</button>
				</div>
			)}
		>
			<Catalog />
		</Errored>
	)
}

render(App)

Rejected promise from a component

A component may return a promise — pota awaits it and renders the resolved value, or, if it rejects, throws the rejection back into the reactive scope where <Errored/> catches it. Clicking reset re-creates the subtree, so AsyncBoom runs again with a fresh promise.

import { render, signal } from 'pota'
import { Errored, Suspense } from 'pota/components'

const ok = signal(false)

function AsyncBoom() {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			ok.read()
				? resolve(<p>Everything is fine now.</p>)
				: reject(new Error('fetch failed'))
		}, 800)
	})
}

render(
	<Errored
		fallback={(err, reset) => (
			<div>
				<p>Something broke: {String(err)}</p>
				<button
					on:click={() => {
						ok.write(true)
						reset()
					}}
				>
					retry
				</button>
			</div>
		)}
	>
		<Suspense fallback={<p>Loading…</p>}>
			<AsyncBoom />
		</Suspense>
	</Errored>,
)