Skip to content

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:

ts
import { createErrorHistory, ErrorHistoryPanel } from 'vue-error-boundary-kit/devtools'

const history = createErrorHistory({ limit: 50 })
vue
<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-api isn't dependency-free itself — it pulls in @vue/devtools-kit and its own dependency tree, and a minimal bundled setupDevtoolsPlugin() 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 inside nuxi dev), but createErrorHistory()'s reactive state lives in the browser app instance, while a devtools tab is registered from the module's setup(), 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's latest on npm is currently a 4.0.0-alpha; the stable line is 3.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):

ts
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'
ts
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:

  • renderToString never throws for a failure inside a boundary.
  • Sibling content renders normally around the failed subtree.
  • The error event 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.