Vue & Svelte Global Error Handlers

Architectural Foundations of Global Error Handling

Modern single-page applications require deterministic error capture to prevent cascading UI failures. Implementing a centralized error strategy ensures that unhandled exceptions are intercepted before they corrupt the DOM tree or leave the application in an unrecoverable state. Within the broader Framework-Specific Crash Recovery & Error Handlers ecosystem, Vue and Svelte adopt fundamentally different paradigms: Vue relies on imperative app-level configuration with explicit lifecycle hooks, while Svelte leans on reactive store propagation and runtime event delegation.

A robust architecture decouples error interception from error rendering. The interception layer captures payloads, normalizes stack traces, and routes them to observability pipelines. The rendering layer consumes normalized state to mount isolated fallback templates. This separation guarantees that telemetry fires synchronously while UI recovery remains non-blocking.

// baseline-error-handler.ts
import type { App } from 'vue';

export interface ErrorPayload {
  error: Error;
  instance: unknown;
  info: string;
  timestamp: number;
}

export function registerGlobalErrorHandler(app: App) {
  app.config.errorHandler = (err, instance, info) => {
    const payload: ErrorPayload = {
      error: err instanceof Error ? err : new Error(String(err)),
      instance,
      info,
      timestamp: Date.now(),
    };
    // Synchronous routing to telemetry/state layer
    window.dispatchEvent(new CustomEvent('app:error', { detail: payload }));
  };
}

Edge Cases

  • Cross-origin iframe errors: Browsers restrict stack trace access for errors originating in cross-origin frames. Implement window.addEventListener('message') with strict origin validation to capture child frame failures.
  • Third-party analytics script failures: External scripts often throw synchronous exceptions during initialization. Wrap third-party SDK injections in try/catch blocks and defer execution using requestIdleCallback where possible.

Pitfalls

  • Overriding native console.error without fallback: Silencing console output removes developer visibility during debugging. Always proxy the original method: const original = console.error; console.error = (...args) => { telemetry(args); original(...args); };
  • Blocking the event loop during synchronous error serialization: Heavy JSON stringification or synchronous DOM parsing inside error handlers will freeze the UI. Offload serialization to setTimeout(fn, 0) or Web Workers.

Vue 3: app.config.errorHandler & Component Interception

Vue 3 exposes app.config.errorHandler as the primary global catch-all. This function receives the error, component instance, and lifecycle hook context. Unlike the declarative class-based approach in React Error Boundary Implementation, Vue’s handler operates imperatively at the app root, requiring explicit state routing to prevent silent crashes. Developers must pair this with component-level onErrorCaptured for granular fallback UI rendering.

The following implementation demonstrates a type-safe integration with Pinia for centralized error state management, alongside a reusable fallback component that leverages Vue’s slot system.

// stores/errorStore.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useErrorStore = defineStore('error', () => {
  const activeError = ref<Error | null>(null);
  const errorContext = ref<string | null>(null);

  function capture(err: Error, context: string) {
    activeError.value = err;
    errorContext.value = context;
  }

  function clear() {
    activeError.value = null;
    errorContext.value = null;
  }

  return { activeError, errorContext, capture, clear };
});



Edge Cases

  • Errors in async setup() functions: Vue’s errorHandler does not automatically catch unhandled promises inside <script setup>. Wrap async operations with explicit .catch() or use a global unhandledrejection listener.
  • SSR hydration mismatches triggering false positives: Differences between server-rendered HTML and client hydration can throw during mount. Implement a hydration-specific error boundary that logs mismatches without crashing the client bundle.

Pitfalls

  • Mutating the error object before logging: Frameworks and telemetry platforms rely on original stack traces. Clone or serialize errors before adding metadata.
  • Forgetting to return false to prevent propagation: Returning false from errorHandler stops the error from bubbling to window.onerror. Omitting this causes duplicate telemetry reports.

Svelte: Runtime Listeners & Reactive Error Stores

Svelte lacks a native global error boundary API, necessitating a custom architecture built on window.addEventListener('error') and writable stores. By centralizing error payloads in a reactive store, components can subscribe to failure states and render isolated fallback templates. When paired with SvelteKit, server and client error boundaries align closely with the routing-level isolation patterns documented in Next.js and Nuxt Routing Error Pages, enabling seamless recovery without full page reloads.

The implementation below establishes a global listener in the application entry point, routes errors through a type-safe store, and demonstrates component-level consumption.

// lib/stores/errorStore.ts
import { writable } from 'svelte/store';

export interface AppError {
  message: string;
  stack?: string;
  timestamp: number;
  recovered: boolean;
}

export const appError = writable<AppError | null>(null);

export function setError(error: unknown) {
  const normalized = error instanceof Error ? error : new Error(String(error));
  appError.set({
    message: normalized.message,
    stack: normalized.stack,
    timestamp: Date.now(),
    recovered: false,
  });
}

export function clearError() {
  appError.set(null);
}
// main.ts
import { setError } from './lib/stores/errorStore';

window.addEventListener('error', (event) => {
  event.preventDefault(); // Prevent default browser console noise
  setError(event.error || new Error(event.message));
});

window.addEventListener('unhandledrejection', (event) => {
  event.preventDefault();
  setError(event.reason || new Error('Unhandled Promise Rejection'));
});



{#if $appError}
 
{:else}
 
{/if}

Edge Cases

  • Transition/animation runtime exceptions: Svelte’s built-in transition engine can throw during rapid DOM updates. Wrap complex transitions in try/catch or disable them during known high-load states.
  • Web Worker message passing failures: Errors thrown inside workers do not bubble to the main thread. Implement worker.onerror handlers that post serialized errors back to the main thread for store ingestion.

Pitfalls

  • Store subscription memory leaks on component unmount: Always use Svelte’s $ store syntax or explicitly call store.subscribe() with a cleanup function in onDestroy.
  • Infinite render loops when error state triggers re-render: If a fallback component’s initialization logic throws, it will recursively trigger the error store. Implement a maximum retry counter or a hard fallback route.

Asynchronous Error Propagation & Telemetry Integration

Promise rejections bypass synchronous error boundaries unless explicitly routed. Developers must implement a global unhandledrejection listener that forwards payloads to observability platforms. For Vue, integrating with onErrorCaptured enables precise async stack tracing, as detailed in Handling async errors in Vue 3 with onErrorCaptured. Telemetry hooks should batch reports and strip sensitive context before transmission to avoid network bottlenecks.

The following utility demonstrates a non-blocking telemetry queue with automatic batching, payload sanitization, and offline resilience.

// lib/telemetry.ts
interface TelemetryEvent {
  type: 'error' | 'warning';
  payload: Record<string, unknown>;
  timestamp: number;
}

const queue: TelemetryEvent[] = [];
const BATCH_SIZE = 5;
const FLUSH_INTERVAL = 3000;

function sanitize(payload: Record<string, unknown>): Record<string, unknown> {
  const { password, token, ...safe } = payload;
  return safe;
}

async function flushQueue() {
  if (queue.length === 0) return;
  const batch = queue.splice(0, BATCH_SIZE);
  try {
    await fetch('/api/telemetry', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(batch),
    });
  } catch {
    // Re-queue on failure; implement exponential backoff in production
    queue.unshift(...batch);
  }
}

export function reportError(error: Error, context: Record<string, unknown>) {
  queue.push({
    type: 'error',
    payload: sanitize({ message: error.message, stack: error.stack, ...context }),
    timestamp: Date.now(),
  });
  if (queue.length >= BATCH_SIZE) flushQueue();
}

// Auto-flush on interval
setInterval(flushQueue, FLUSH_INTERVAL);

Edge Cases

  • Race conditions during concurrent fetches: Multiple async operations failing simultaneously can trigger overlapping telemetry requests. Use a mutex or atomic counter to serialize flush operations.
  • Offline state during error reporting: Browsers queue fetch requests when offline. Implement navigator.onLine checks and fallback to localStorage buffering with a sync event listener.

Pitfalls

  • Double-capturing across global and component levels: If both errorHandler and onErrorCaptured report to the same endpoint, you will inflate error counts. Implement a reported: boolean flag on the error object or use a centralized deduplication service.
  • Exposing internal stack traces to production clients: Never render raw stack traces in fallback UIs. Strip them before transmission and log only sanitized hashes or error codes.

Session State Preservation & Graceful Degradation

Crash recovery requires deterministic state synchronization. Implement pre-operation snapshots using IndexedDB or sessionStorage to preserve form inputs, scroll positions, and navigation history. When an error boundary activates, the UX should degrade gracefully by restoring the last known stable state rather than rendering a blank screen. This pattern ensures continuity for QA validation and end-user trust.

The middleware and router guard below demonstrate how to intercept state mutations, serialize critical data, and trigger recovery flows.

// lib/statePersistence.ts
import { get, set, del } from 'idb-keyval';

const SNAPSHOT_KEY = 'app:snapshot:v1';

export async function takeSnapshot(state: Record<string, unknown>) {
  try {
    await set(SNAPSHOT_KEY, JSON.stringify(state));
  } catch (err) {
    console.warn('Snapshot failed:', err);
  }
}

export async function restoreSnapshot(): Promise<Record<string, unknown> | null> {
  try {
    const raw = await get(SNAPSHOT_KEY);
    if (!raw) return null;
    return JSON.parse(raw as string);
  } catch {
    await del(SNAPSHOT_KEY);
    return null;
  }
}
// router/guards.ts
import { restoreSnapshot } from '@/lib/statePersistence';

export async function recoveryGuard(to: RouteLocationNormalized) {
  if (to.query.recover === 'true') {
    const state = await restoreSnapshot();
    if (state) {
      // Inject state into store before component mount
      store.dispatch('hydrate', state);
      return { query: { ...to.query, recover: undefined } };
    }
  }
}

Edge Cases

  • Storage quota exceeded during snapshot: Browsers impose strict limits on sessionStorage and IndexedDB. Implement size estimation before serialization and fallback to critical-field-only snapshots when limits are approached.
  • Corrupted state payloads: JSON parsing can fail if the stored string is truncated. Always wrap restoration in try/catch and invalidate corrupted keys immediately.

Pitfalls

  • Storing PII in unencrypted local storage: Never persist authentication tokens, emails, or financial data without encryption. Use framework-specific secure storage APIs or ephemeral memory stores.
  • Excessive serialization overhead on high-frequency updates: Debounce snapshot triggers or use structural sharing (e.g., Immer) to only persist changed branches of the state tree.

Automated Testing & Fault Injection Strategies

QA teams must validate error boundaries using deterministic fault injection. Playwright and Cypress can simulate network failures, DOM mutations, and async rejections to verify that fallback UIs render correctly and telemetry fires without blocking the main thread. Test suites should assert that error states do not leak into subsequent test runs.

The following examples demonstrate framework-agnostic fault injection patterns using Vitest, Playwright, and Cypress.

// tests/vitest/errorInjector.spec.ts
import { vi } from 'vitest';
import { setError } from '@/lib/stores/errorStore';

describe('Error Boundary Fault Injection', () => {
  it('should render fallback and clear state on recovery', async () => {
    const mockError = new Error('Simulated DOM mutation failure');
    setError(mockError);

    // Assert store state
    expect(get(appError)).toMatchObject({ message: mockError.message });

    // Simulate user recovery action
    clearError();
    expect(get(appError)).toBeNull();
  });
});
// tests/playwright/networkFailure.spec.ts
import { test, expect } from '@playwright/test';

test('graceful degradation on API failure', async ({ page }) => {
  await page.route('**/api/data', (route) => route.abort('failed'));
  await page.goto('/dashboard');

  await expect(page.locator('[role="alert"]')).toBeVisible();
  await expect(page.locator('text=Try Again')).toBeVisible();
});
// tests/cypress/errorBoundary.cy.ts
describe('Error Boundary Assertions', () => {
  it('prevents main thread blocking during crash', () => {
    cy.window().then((win) => {
      win.dispatchEvent(new ErrorEvent('error', { error: new Error('Test') }));
    });

    cy.get('.error-fallback').should('be.visible');
    cy.get('button').contains('Recover').click();
    cy.get('.error-fallback').should('not.exist');
  });
});

Edge Cases

  • Flaky async test timing: Error propagation often involves microtask queues. Use explicit await on promises or framework-specific waitFor utilities to avoid race conditions in assertions.
  • Headless browser memory limits during crash loops: Repeatedly throwing errors in CI environments can exhaust V8 heap space. Implement test teardown hooks that explicitly clear error stores and reset DOM state.

Pitfalls

  • Testing only happy paths: Error boundaries are only as reliable as their failure scenarios. Mandate a minimum 30% fault-injection coverage in CI pipelines.
  • Ignoring error boundary cleanup lifecycle: Failing to reset global listeners or stores between tests causes state pollution. Always use beforeEach/afterEach hooks to isolate error contexts.

Frequently Asked Questions

How do I prevent memory leaks in global error listeners? Attach listeners during application initialization and explicitly detach them during teardown or hot-module replacement (HMR) cycles. In Vue, use app.config.errorHandler assignment rather than addEventListener where possible. In Svelte, ensure window.removeEventListener is called in onDestroy if you manually attach listeners.

What is the performance impact of synchronous error serialization? Synchronous JSON.stringify on deeply nested error objects can block the main thread for 10–50ms depending on object size. Mitigate this by extracting only message, name, and a truncated stack string before serialization. Offload heavy payload construction to requestIdleCallback or a dedicated Web Worker.

Can I share error boundary logic between Vue and Svelte micro-frontends? Yes, by abstracting the interception and telemetry layers into framework-agnostic ES modules. Share the TelemetryEvent interface, the batching queue, and the state persistence utilities. Each micro-frontend should only implement its own UI binding layer (e.g., Vue’s errorHandler vs Svelte’s window listener) while routing to the shared core.

How do I handle Web Component shadow DOM errors within framework boundaries? Errors thrown inside shadow DOM do not bubble to the light DOM by default. Attach error event listeners directly to the custom element instance or use Element.prototype.attachShadow wrappers to intercept exceptions. Forward caught errors to the host framework’s global store using CustomEvent dispatching across the shadow boundary.