Skip to the solution
MediumFrontend10 min read

How do you handle errors in Vue (onErrorCaptured, app.config.errorHandler)?

755 views

The problem

Question presented to candidate: "One broken widget on our dashboard blanks part of the page, and we only hear about production errors from angry users. How does error handling work in Vue, and how would you contain failures and report them?"

What a strong answer should cover:

  • app.config.errorHandler(err, instance, info): the global handler for errors Vue catches (render, setup, lifecycle hooks, watchers, component event handlers, including async ones); the place to report to a monitoring service.
  • onErrorCaptured(err, instance, info) in an ancestor: called for errors from descendants; returning false stops propagation, which is how an error boundary shows a fallback for one widget.
  • The propagation order: nearest ancestor hook first, then further ancestors, then app.config.errorHandler.
  • What Vue cannot catch: code it does not call, such as setTimeout callbacks and other unrelated async work; those need window error and unhandledrejection listeners.
  • Production details: info becomes a short error-reference URL instead of a description; unhandled errors are logged, not thrown, unless app.config.throwUnhandledErrorInProduction is set (Vue 3.5+).

Clarifying questions expected:

  • "Should one failed widget show a fallback while the rest of the page keeps working?"
  • "Which monitoring service do we report to, and do we need component context in the report?"

Code / implementation expected: Yes β€” an ErrorBoundary component using onErrorCaptured, and a global errorHandler in main.ts.

  • vue
  • errors
  • error-handling

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 9 sections

Read the code

VueA reusable error boundary with onErrorCaptured and a reset button
<script setup>
import { defineComponent, h, ref, onErrorCaptured } from "vue";

// A reusable error boundary: catches errors from its descendants,
// shows a fallback instead of the broken subtree, and offers a reset.
const ErrorBoundary = defineComponent({
  name: "ErrorBoundary",
  props: { name: String },
  setup(props, { slots }) {
    const error = ref(null);
    const resetKey = ref(0);
    onErrorCaptured((err, instance, info) => {
      error.value = err;
      log.value.push(`${props.name} caught "${err.message}" (${info})`);
      return false; // handled here: do not bubble to parents or app.config.errorHandler
    });
    const reset = () => { error.value = null; resetKey.value++; };
    return () =>
      error.value
        ? h("div", { role: "alert" }, [`${props.name} failed: ${error.value.message} `, h("button", { onClick: reset }, "Try again")])
        : h("div", { key: resetKey.value }, slots.default?.()); // new key = fresh children
  },
});

// A child that breaks while rendering once its count reaches 3.
const FlakyCounter = defineComponent({
  name: "FlakyCounter",
  setup() {
    const count = ref(0);
    return () => {
      if (count.value === 3) throw new Error("count 3 is cursed");
      return h("button", { onClick: () => count.value++ }, `count: ${count.value}`);
    };
  },
});

// A child whose click handler throws (the render itself is fine).
const BadButton = defineComponent({
  name: "BadButton",
  setup() {
    const save = () => { throw new Error("save failed"); };
    return () => h("button", { onClick: save }, "Save");
  },
});

const log = ref([]);
</script>

<template>
  <ErrorBoundary name="Widget A"><FlakyCounter /></ErrorBoundary>
  <ErrorBoundary name="Widget B"><BadButton /></ErrorBoundary>
  <ol>
    <li v-for="(line, i) in log" :key="i">{{ line }}</li>
  </ol>
</template>
Script that prints how an error travels: child, boundaries, global handler
import "./dom.mjs";
import { createApp, defineComponent, h, ref, onErrorCaptured, onMounted, nextTick, watch } from "vue";
const log = (...a) => console.log(...a);
const wait = (ms = 20) => new Promise((r) => setTimeout(r, ms));

const Child = defineComponent({
  name: "Child",
  setup() {
    const mode = ref("ok");
    const boom = () => { throw new Error("click handler failed"); };
    const asyncBoom = async () => { await null; throw new Error("async handler failed"); };
    watch(mode, (m) => { if (m === "watch") throw new Error("watcher failed"); });
    return () => {
      if (mode.value === "render") throw new Error("render failed");
      return h("div", [
        h("button", { id: "render", onClick: () => (mode.value = "render") }, "r"),
        h("button", { id: "sync", onClick: boom }, "s"),
        h("button", { id: "async", onClick: asyncBoom }, "a"),
        h("button", { id: "watch", onClick: () => (mode.value = "watch") }, "w"),
        h("button", { id: "timer", onClick: () => setTimeout(() => { throw new Error("timer failed"); }) }, "t"),
      ]);
    };
  },
});
const Middle = defineComponent({
  name: "Middle",
  setup() {
    onErrorCaptured((err, instance, info) => {
      log(`  1. Middle onErrorCaptured: "${err.message}" from <${instance?.$options.name}> info="${info}"`);
      if (err.message.startsWith("watcher")) { log("     Middle returns false: propagation stops"); return false; }
    });
    return () => h(Child);
  },
});
const App = defineComponent({
  setup() {
    onErrorCaptured((err) => { log(`  2. App onErrorCaptured: "${err.message}"`); });
    return () => h(Middle);
  },
});

const app = createApp(App);
app.config.errorHandler = (err, instance, info) => log(`  3. app.config.errorHandler: "${err.message}" info="${info}"`);
process.on("uncaughtException", (e) => log(`  !! uncaughtException (Vue never saw it): "${e.message}"`));
app.mount("#app");
for (const id of ["sync", "async", "watch", "timer", "render"]) {
  log(`click #${id}`);
  document.getElementById(id).click();
  await wait();
}
log("DOM after render error:", JSON.stringify(document.querySelector("#app").innerHTML));
process.exit(0);

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 38 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

How do you access a DOM element or child component instance?

Continue