Advanced usage
Debugging: error history
/devtools — not a Vue Devtools browser-extension integration, but a small, dependency-free in-memory history you can drop into a page during development:
import { createErrorHistory, ErrorHistoryPanel } from 'vue-error-boundary-kit/devtools'
const history = createErrorHistory({ limit: 50 })<ErrorBoundary :reporter="[sentryReporter, history.record]">…</ErrorBoundary>
<ErrorHistoryPanel v-if="isDev" :history="history" />history.record is itself an ErrorReporter — it rides the existing reporter mechanism, so no other wiring is needed. history.entries is a reactive, most-recent-first array (capped at limit, default 50); <ErrorHistoryPanel> is an optional, dependency-free component that renders it (inline-styled, no separate CSS import needed) with a "Clear" button.
Why not a real Vue Devtools extension integration? Considered and declined.
@vue/devtools-apiisn't dependency-free itself — it pulls in@vue/devtools-kitand its own dependency tree, and a minimal bundledsetupDevtoolsPlugin()call measures ~20 kB gzip on its own, over 9× this package's entire core budget (~2.1 kB gzip). Unlike the Sentry/Bugsnag/LogRocket adapters, there's no way to depend on it "structurally" without bundling it — a devtools inspector has no already-initialized external client to defer to.<ErrorHistoryPanel>covers the same debugging need without that cost, and without requiring the extension to be installed at all.
What about a custom Nuxt DevTools tab (
@nuxt/devtools-kit)? Also considered and declined — not the same thing as the browser-extension question above, but the same conclusion.addCustomTab()is real and would cost nothing in production (it only runs insidenuxi dev), butcreateErrorHistory()'s reactive state lives in the browser app instance, while a devtools tab is registered from the module'ssetup(), which runs in Node.js. The only view type that supports live data (iframe, pointed at a served dev-server route) needs its own client↔devtools-server RPC bridge (extendServerRpc/iframe-client) — a small SPA and protocol of its own, not a quick addition — and the ecosystem is mid-major-version-transition (@nuxt/devtools-kit'slateston npm is currently a4.0.0-alpha; the stable line is3.4.1).<ErrorHistoryPanel>already covers the same need everywhere, DevTools open or not.
Testing your app
vue-error-boundary-kit/testing — test doubles and fixtures for exercising code that uses <ErrorBoundary>/useErrorBoundary()/an adapter, framework-agnostic beyond Vue itself (no vi.fn()/jest.fn() dependency baked in, so it works the same under Vitest or Jest):
import {
ThrowInRender, // throws in render() when `shouldThrow` (default true); prop `message`
ThrowInSetup, // throws synchronously in setup() — source: 'render'
ThrowInAsyncSetup, // throws after an await in an async setup() — source: 'async'; render inside <Suspense>/<AsyncBoundary>
ThrowAbortError, // throws an AbortError DOMException, matching a cancelled fetch()
makeCapturedError, // build a CapturedError fixture for a reporter/adapter unit test
createRecordingReporter, // an ErrorReporter test double: { calls, report(), reset() }
} from 'vue-error-boundary-kit/testing'import { mount } from '@vue/test-utils'
import { h, nextTick } from 'vue'
import { ErrorBoundary } from 'vue-error-boundary-kit'
import { ThrowInRender, createRecordingReporter } from 'vue-error-boundary-kit/testing'
const reporter = createRecordingReporter()
const wrapper = mount(ErrorBoundary, {
props: { reporter },
slots: {
default: () => h(ThrowInRender, { message: 'boom' }),
fallback: ({ error }) => h('div', { class: 'fallback' }, error.message),
},
})
await nextTick() // onErrorCaptured sets reactive state synchronously; the DOM swap is not
expect(wrapper.find('.fallback').text()).toBe('boom')
expect(reporter.calls[0]?.error.source).toBe('render')SSR notes
<ErrorBoundary> is SSR-safe in the sense that matters most: a failing subtree never crashes renderToString or turns into a full 500 page, and error events/reporters fire correctly on the server exactly like on the client.
There is one honest limitation worth knowing, rooted in how Vue's SSR renderer works rather than in this package: on the client, onErrorCaptured setting reactive state triggers a genuine second render pass, so the fallback slot's markup replaces the failed content. Vue's server renderer has no equivalent "re-render" step — a component's render() has already returned by the time a descendant's failure is caught, so the server HTML for that specific position comes out as an empty placeholder rather than the fallback slot's own markup. Achieving pixel-perfect SSR fallback HTML would require either internal renderer APIs or re-executing the failing subtree's setup() a second time — both of which this package deliberately avoids (see Vapor-mode readiness below).
This was verified empirically, not just assumed: wrapping the default slot in <Suspense> doesn't change the outcome either, for both a synchronous throw and a rejected async setup() — <Suspense>'s SSR buffering only defers unresolved async dependencies so it can commit its #default branch once they settle; it has no mechanism, public or private, to commit its #fallback branch when a dependency rejects instead. A bare <Suspense> with no error boundary around it at all, whose #default branch's setup() rejects, still serializes to an empty placeholder — confirming this isn't specific to how this package uses onErrorCaptured.
What is guaranteed, and covered by tests:
renderToStringnever throws for a failure inside a boundary.- Sibling content renders normally around the failed subtree.
- The
errorevent and any configured reporter fire exactly once, on the server, just like on the client. - Hydration always converges on correct, interactive client-side content — even if the server and client end up disagreeing about whether a given subtree failed (e.g. a fetch that failed only on the server has since succeeded by the time the client hydrates). Vue's own hydration-mismatch recovery may log its standard dev-only warning in that disagreement case (stripped from production builds); the end state is always correct.