VueA reusable error boundary with onErrorCaptured and a reset button
<script setup>
import { defineComponent, h, ref, onErrorCaptured } from "vue";
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;
});
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?.());
},
});
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}`);
};
},
});
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);