API reference
<ErrorBoundary>
| Prop | Type | Default | Description |
|---|---|---|---|
resetKeys | unknown[] | — | When any value changes (compared with Object.is), the boundary auto-resets — same idea as react-error-boundary's resetKeys |
resetOnPropsChange | boolean | false | Reset whenever any prop reference changes, not just resetKeys |
beforeReset | () => void | — | Called right before a reset (auto or manual). Not named onReset — see note below |
isolate | boolean | true | false lets the error also propagate to the nearest ancestor <ErrorBoundary> |
maxRetries | number | unlimited | Once reached, the fallback slot's canRetry becomes false |
reporter | ErrorReporter | ErrorReporter[] | — | Reporter(s) invoked once per captured error — see Reporting adapters |
shouldCatch | (error: CapturedError) => boolean | — | Return false to let an error pass through this boundary untouched — no state change, no report, no fallback — exactly as if it weren't there. See Ignoring specific errors |
internalErrorPrefix | string | '[vue-error-boundary-kit]' | Prefix for the safety-net log emitted when your own beforeReset/reporter itself throws. Pass '' to omit it |
Why
beforeReset, notonReset? This component also emits aresetevent, and Vue derivesonResetas that event's own listener prop. A declared prop with the identical name would collide with it: Vue'semit()looks upprops.onResetindependently of whether it's "really" a declared prop, so it would've fired twice per reset — and the second, emit-triggered call happens outside this package's own try/catch, defeating the recursion guard entirely if it throws.beforeResetavoids the collision structurally.
Events:
error(error: CapturedError)— fired for every captured failure, including ones bubbled up from a descendant boundary withisolate: false.reset()— fired on every manual or automatic reset.
Slots:
default— normal content.fallback— scoped slot:{ error, reset, retry, retryCount, canRetry }.reset()clears retry count too;retry()incrementsretryCountand re-attemptsdefaultwithout resetting the count.
Exposed (via a template ref): error, hasError, retryCount, canRetry, reset(), retry() — the same state and methods the fallback slot gets, but reachable from outside it (e.g. a retry control that lives elsewhere in the UI, or an ancestor recovering a boundary it doesn't render the fallback for). Calling reset()/retry() on a boundary that isn't in an error state is a harmless no-op.
<script setup>
const boundary = ref()
</script>
<template>
<ErrorBoundary ref="boundary">…</ErrorBoundary>
<button @click="boundary?.reset()">Reset from elsewhere</button>
</template>Retry with backoff
retry() is instant and unconditional — fine for most cases, but for a transient/network-ish failure, retrying the instant the button is clicked usually just fails again the same way. vue-error-boundary-kit/retry-backoff wraps a template ref's retry() with an increasing delay instead:
import { createBackoffRetry } from 'vue-error-boundary-kit/retry-backoff'
const boundary = useTemplateRef('boundary')
const backoff = createBackoffRetry(boundary, { baseDelayMs: 1000, factor: 2, maxDelayMs: 30_000 })<ErrorBoundary ref="boundary">
<template #fallback="{ error }">
<button :disabled="backoff.isPending.value" @click="backoff.retry()">
{{ backoff.isPending.value ? 'Retrying…' : 'Retry' }}
</button>
</template>
</ErrorBoundary>The delay is computed from the boundary's own retryCount (baseDelayMs * factor ** retryCount, capped at maxDelayMs), so each successive attempt waits longer. cancel() clears a pending retry. Works with <AsyncBoundary>'s ref too — both expose the same { retry, retryCount } shape.
<AsyncBoundary>
Separate entry point (vue-error-boundary-kit/async-boundary) — not part of the core bundle, so it costs nothing if you don't import it. Combines <Suspense> and <ErrorBoundary>, which today you'd otherwise nest by hand:
import { AsyncBoundary } from 'vue-error-boundary-kit/async-boundary'<AsyncBoundary :reset-keys="[userId]">
<template #default>
<UserProfile :id="userId" />
<!-- async setup() / async components allowed -->
</template>
<template #loading>
<Spinner />
</template>
<template #fallback="{ error, retry }">
<ErrorState :message="error.message" @retry="retry" />
</template>
</AsyncBoundary>It's a composition, not a reimplementation: internally it's <ErrorBoundary> wrapping a <Suspense>, so it accepts every <ErrorBoundary> prop (resetKeys, maxRetries, reporter, shouldCatch, …), emits the same error/reset events, and exposes the same error/hasError/retryCount/canRetry/reset()/retry() via a template ref — all handled by the one onErrorCaptured implementation <ErrorBoundary> already has. The only thing it adds is the loading slot, rendered while the default slot's async dependencies are pending. retry()/reset() remount the default slot, so a retried async operation genuinely re-runs (the loading slot reappears while it does) rather than just re-showing stale state.
useErrorBoundary()
For programmatic use outside a template <ErrorBoundary> — e.g. a custom layout-level error state in Nuxt, or registering errors from code that errorCaptured never sees:
const { error, hasError, reset, captureError } = useErrorBoundary({
onError: (e) => report(e),
reporter: myReporter,
})
try {
JSON.parse(untrustedInput)
} catch (err) {
captureError(err, { source: 'manual', componentName: 'ImportPanel' })
}captureError(err, info?)— registers an error manually; returns the resultingCapturedError.reset()— clearserrorback tonull.error: ShallowRef<CapturedError | null>,hasError: ComputedRef<boolean>.
Options: onError, beforeReset (called right before reset() clears state), reporter, reportContext, internalErrorPrefix (see the <ErrorBoundary> note above — same default, same reasoning, though there's no naming collision risk here since this is a plain composable, not a component with its own reset emit).
useGlobalErrorCapture()
Separate entry point (vue-error-boundary-kit/global-capture) — not wired up by default, so it costs nothing in bundles that don't import it (including SSR bundles, where it's a no-op if window isn't defined). Wires up window.addEventListener('error', …) and unhandledrejection, funneled through the same reporter/onError pattern:
import { useGlobalErrorCapture } from 'vue-error-boundary-kit/global-capture'
useGlobalErrorCapture({
reporter: myReporter,
onError: (e) => console.warn('uncaught:', e),
})Options: onError, reporter, reportContext, shouldCatch, internalErrorPrefix, captureErrors (default true), captureRejections (default true). Returns { stop }; cleanup also runs automatically if called inside an active effect scope (e.g. a component's setup()).
Types
interface CapturedError {
error: unknown
message: string
stack?: string
componentName?: string
lifecycleHook?: string
source: 'render' | 'async' | 'event' | 'unhandledrejection' | 'manual'
timestamp: number
}
interface ErrorReporter {
report(error: CapturedError, context?: Record<string, unknown>): void | Promise<void>
}source notes:
'render'— a synchronous failure during a component's render or (sync)setup().'async'— anasync setup()that rejects after anawait. Vue reports both cases with the identical"setup function"info string, so this package additionally checks whether the component's ownsetupis anAsyncFunctionto tell them apart.'event'— a Vue-compiledv-onhandler (native DOM event or componentemit) that throws. Note: Vue does route these throughonErrorCaptured— what it genuinely can't see is covered below.'unhandledrejection'/ and non-Vue'event's — only produced byuseGlobalErrorCapture().'manual'— the default forcaptureError()when nosourceis given.