Post contents
Despite our best efforts, bugs will find their way into our applications. Unfortunately, we can't simply ignore them, or the user experience suffers greatly.
Take the following code:
- React
- Angular
- Vue
const App = () => { const items = [ { id: 1, name: "Take out the trash", priority: 1 }, { id: 2, name: "Cook dinner", priority: 1 }, { id: 3, name: "Play video games", priority: 2 }, ]; const priorityItems = items.filter((item) => item.item.priority === 1); return ( <> <h1>To-do items</h1> <ul> {priorityItems.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> </> );};
@Component({ selector: "app-root", standalone: true, template: ` <h1>To-do items</h1> <ul> @for (item of priorityItems; track item.id) { <li>{{ item.name }}</li> } </ul> `,})class AppComponent { items = [ { id: 1, name: "Take out the trash", priority: 1 }, { id: 2, name: "Cook dinner", priority: 1 }, { id: 3, name: "Play video games", priority: 2 }, ]; priorityItems = this.items.filter((item: any) => item.item.priority === 1);}
<!-- App.vue --><script setup>const items = [ { id: 1, name: "Take out the trash", priority: 1 }, { id: 2, name: "Cook dinner", priority: 1 }, { id: 3, name: "Play video games", priority: 2 },];const priorityItems = items.filter((item) => item.item.priority === 1);</script><template> <h1>To-do items</h1> <ul> <li v-for="item of priorityItems" :key="item.id">{{ item.name }}</li> </ul></template>
Without running the code, everything looks pretty good, right?
Maybe you've spotted the error by now — that's great! Remember that we all make these small mistakes from time-to-time. Don't dismiss the idea of error handling out of hand as we go forward.
But oh no! When you run the application, it's not showing the h1
or any of the list items like we would expect it to.
The reason those items aren't showing on-screen is that an error is being thrown. Open your console on any of these examples, and you'll find an error waiting for you:
Error: can't access property "priority", item.item is undefined
Luckily, this error is a fairly easy fix, but even if we do, bugs will inevitably be introduced into our apps. A white screen is a pretty subpar experience for our end users — they likely won't even understand what happened that led them to this broken page.
While I doubt we'll ever convince our users that an error is a good thing, how can we make this user experience better, at least?
Before we do that, however, let's explore why throwing an error causes the rendering of a page to fail.
Throwing Errors Causes Blank Screens?!
As shown before, when an error is thrown during a component's render step, it will fail to render any of the contents from the component's template. This means that the following will throw an error and prevent rendering from occurring:
- React
- Angular
- Vue
const ErrorThrowingComponent = () => { throw new Error("Error"); return <p>Hello, world!</p>;};
@Component({ selector: "error-throwing", standalone: true, template: ` <p>Hello, world!</p> `,})class ErrorThrowingComponent { constructor() { throw new Error("Error"); }}
<!-- ErrorThrowing.vue --><script setup>throw new Error("Error");</script><template> <p>Hello, world!</p></template>
However, if we change our code to throw an error during an event handler, the contents will render fine but fail to execute the logic of said error handler:
- React
- Angular
- Vue
const EventErrorThrowingComponent = () => { const onClick = () => { throw new Error("Error"); }; return <button onClick={onClick}>Click me</button>;};
@Component({ selector: "error-throwing", standalone: true, template: ` <button (click)="onClick()">Click me</button> `,})class ErrorThrowingComponent { onClick() { throw new Error("Error"); }}
<!-- ErrorThrowing.vue --><script setup>const onClick = () => { throw new Error("Error");};</script><template> <button @click="onClick()">Click me</button></template>
This behavior may seem strange until you consider how JavaScript's throw
clause works. When a JavaScript function throws an error, it also acts as an early return of sorts.
function getRandomNumber() { // Try commenting this line and seeing the different behavior throw new Error("There was an error"); // Anything below the "throw" clause will not run console.log("Generating a random number"); // This means that values returned after a thrown error are not utilized return Math.floor(Math.random() * 10);}try { const val = getRandomNumber(); // This will never execute because the `throw` bypasses it console.log("I got the random number of:", val);} catch (e) { // This will always run instead console.log("There was an error:", e);}
Moreover, these errors exceed past their scope, meaning that they will bubble up the execution stack.
What does that mean in English?
In practical terms, this means that a thrown error will exceed the bounds of the function you called it in and make its way further up the list of functions you called to get to the thrown error.
function getBaseNumber() { // Error occurs here, throws it upwards throw new Error("There was an error"); return 10;}function getRandomNumber() { // Error occurs here, throws it upwards return Math.floor(Math.random() * getBaseNumber());}function getRandomTodoItem() { const items = [ "Go to the gym", "Play video games", "Work on book", "Program", ]; // Error occurs here, throws it upwards const randNum = getRandomNumber(); return items[randNum % items.length];}function getDaySchedule() { let schedule = []; for (let i = 0; i < 3; i++) { schedule.push( // First execution will throw this error upwards getRandomTodoItem(), ); } return schedule;}function main() { try { console.log(getDaySchedule()); } catch (e) { // Only now will the error be stopped console.log("An error occurred:", e); }}
Because of these two properties of errors, React, Angular, and Vue are unable to "recover" (continue rendering after an error has occurred) from an error thrown during a render cycle.
Errors Thrown in Event Handlers
Conversely, due to the nature of event handlers, these frameworks don't need to handle errors that occur during event handlers. Assume we have the following code in an HTML file:
<!-- index.html --><button id="btn">Click me</button><script> const el = document.getElementById("btn"); el.addEventListener("click", () => { throw new Error("There was an error"); });</script>
When you click on the <button>
here, it will throw an error, but this error will not escape out of the event listener's scope. This means that the following will not work:
try { const el = document.getElementById("btn"); el.addEventListener("click", () => { throw new Error("There was an error"); });} catch (error) { // This will not ever run with this code alert("We're catching an error in try/catch");}
So to catch an error in an event handler, React, Angular, or Vue would have to add a window 'error'
listener, like so:
const el = document.getElementById("btn");el.addEventListener("click", () => { throw new Error("There was an error");});window.addEventListener("error", (event) => { const error = event.error; alert("We're catching an error in another addEventListener");});
But let's think about what adding this window
listener would mean:
- More complex code in the respective frameworks
- Harder to maintain
- Larger bundle size
- When the user clicks on a faulty button, the whole component crashes rather than a single aspect of it failing
This doesn't seem worth the tradeoffs when we're able to add our own try/catch
handlers inside event handlers.
After all, a partially broken application is better than a fully broken one!
Errors Thrown in Other APIs
This property of an error being thrown in an error handler not preventing a render transfers to other aspects of these frameworks as well:
- React
- Angular
- Vue
While some other frameworks catch errors inside async APIs (like React's useEffect
), React will not recover from an error thrown in any of the built-in React Hooks covered so far:
const App = () => { // This will prevent rendering const stateVal = useState(() => { throw new Error("Error in state initialization function"); }); // This will also prevent rendering const memoVal = useMemo(() => { throw new Error("Error in memo"); }, []); // Will this prevent rendering? You bet! useEffect(() => { throw new Error("Error in useEffect"); }); // Oh, and this will too. useLayoutEffect(() => { throw new Error("Error in useEffect"); }); return <p>Hello, world!</p>;};
Despite errors thrown in a component's constructor preventing rendering:
@Component({ selector: "app-root", standalone: true, template: ` <p>Hello, world!</p> `,})class AppComponent { // This will prevent rendering constructor() { throw new Error("Error in constructor"); }}
Errors thrown in any of Angular's other lifecycle methods will not prevent a component from rendering:
@Component({ selector: "app-root", standalone: true, template: ` <p>Hello, world!</p> `,})class AppComponent implements OnInit { // Will not prevent `Hello, world!` from showing ngOnInit() { throw new Error("Error in constructor"); }}
While some of Vue's APIs, like watchEffect
or computed
will prevent rendering when an error occurs:
<!-- App.vue --><script setup>import { watchEffect, computed } from "vue";// This will prevent renderingwatchEffect(() => { throw new Error("New error in effect");});// This will also prevent renderingconst result = computed(() => { throw new Error("New error in computed");});// "computed" is lazy, meaning that it will not throw the error// (or compute a result) unless the value is used, like so:console.log(result.value);</script><template> <p>Hello, world!</p></template>
Other APIs, like the onMounted
lifecycle method, will not prevent rendering when an error is thrown inside it:
<!-- App.vue --><script setup>import { onMounted } from "vue";onMounted(() => { // Will not prevent `Hello, world!` from showing throw new Error("New error");});</script><template> <p>Hello, world!</p></template>
While this might seem confusing at first, it makes sense when you consider when onMounted
runs when compared with computed
, for example.
See, while computed
runs during the setup of the component, onMounted
executes after the component has been rendered. Because of this, Vue is able to recover from errors thrown during onMounted
and afterward but not during those thrown in a render function.
Now that we understand why these errors prevent you from rendering content, let's see how we're able to improve the user experience when errors do occur.
Logging Errors
The first step to providing a better end-user experience when it comes to errors is to reduce how many are made.
Well, duh
Sure, this seems obvious, but consider this: If an error occurs on the user's machine and it isn't caught during internally, how are you supposed to know how to fix it?
This is where the concept of "logging" comes into play. The general idea behind logging is that you can capture a collection of errors and information about the events that led up to the errors. You want to provide a way to export this data so that your user can send it to you to debug.
While this logging often involves submitting data to a server, let's keep things local to the user's machine for now.
- React
- Angular
- Vue
Up to this point, all of our React components have been functions. While this is how most modern React applications are built today, there is another way of writing a React component; this being the "class" API.
Class-based React components existed well before function components. Class-based components were in React since day one, and functional components were only truly made viable with a significant revamp in React 16.8, coinciding with the introduction of React Hooks.
Here's a simple React component in both functional and class-based APIs:
// Function componentconst FnCounter = (props) => { // Setting up state const [count, setCount] = useState(0); // Function to update state const addOne = () => setCount(count + 1); // Rendered UI via JSX return ( <div> <p>You have pushed the button {count} times</p> <button onClick={addOne}>Add one</button> {/* Using props to project children */} {props.children} </div> );};
// Class componentimport { Component } from "react";class ClassCounter extends Component { // Setting up state state = { count: 0 }; // Function to update state addOne() { // Notice we use an object and `setState` to update state this.setState({ count: this.state.count + 1 }); } // Rendered UI via JSX render() { return ( <div> <p>You have pushed the button {this.state.count} times</p> <button onClick={() => this.addOne()}>Add one</button> {/* Using props to project children */} {this.props.children} </div> ); }}
Both of these components work exactly the same, with no functional differences between them. This is because almost every API that was available to class components made its way over to functional components through React Hooks.
Almost every API made the migration to Hooks.
One of the few exceptions to that rule is the ability to catch and track errors that are thrown within a React application.
Use Class Components to Build an Error Boundary
Now that we understand what a class component is and why it's required to use one for error handling, let's build one ourselves!
Just like any other class component, we start with an extends
clause to tell React that this class is, in fact, a component.
From there, we add a special componentDidCatch
method, like so:
import { Component } from "react";class ErrorBoundary extends Component { componentDidCatch(error, errorInfo) { // Do something with the error console.log(error, errorInfo); } render() { return this.props.children; }}
This method is then called any time a child component throws an error.
Luckily for us, we can mix and match class components and function components. This means that we can demonstrate the componentDidCatch
handler using the following code:
const ErrorThrowingComponent = () => { // This is an example of an error being thrown throw new Error("Error");};const App = () => { return ( <ErrorBoundary> <ErrorThrowingComponent /> </ErrorBoundary> );};
Now, while our screen will still be white when the error is thrown, it will hit our componentDidCatch
handler as we would expect.
Angular uses its dependency injection system to allow developers to keep track of errors as they occur.
However, to provide the custom error handler service, you must provide it at the root of your application, meaning that you cannot simply provide it from your parent component.
class MyErrorHandler implements ErrorHandler { handleError(error: unknown) { // Do something with the error console.log(error); }}// ...bootstrapApplication(AppComponent, { providers: [{ provide: ErrorHandler, useClass: MyErrorHandler }],});
It'd be awesome to have a per-component implementation of
ErrorHandler
, but this feature has not made it into Angular yet.
Now that we've set up our ErrorHandler
instance, we can test that it works using a component that throws an error:
@Component({ selector: "child-comp", standalone: true, template: `<p>Testing</p>`,})class ChildComponent implements OnInit { ngOnInit() { // This is an example of an error being thrown throw new Error("Test"); }}@Component({ selector: "app-root", standalone: true, imports: [ChildComponent], template: ` <child-comp /> `,})class AppComponent {}
Vue enables us to track errors in an application with a simple onErrorCaptured
composition API.
<!-- App.vue --><script setup>import { onErrorCaptured } from "vue";import Child from "./Child.vue";onErrorCaptured((err, instance, info) => { // Do something with the error console.log(err, instance, info);});</script><template> <Child /></template>
Now, when we throw an error inside a child component, like so:
<!-- Child.vue --><script setup>throw new Error("Test");</script><template> <p>Hello, world!</p></template>
It will run the function inside of onErrorCaptured
.
Great! We're now able to keep track of what errors are occurring in our app. Hopefully, this allows us to address bugs as the user experiences them, making the app feel more stable as time goes on.
Now, let's see if we're not able to make the experience a bit nicer for our users when they do hit an error.
Ignoring the Error
Some bugs? They're showstoppers. When they happen, you can't do anything to recover from the error, and as a result, you have to halt the user's ability to interact with the page.
Other bugs, on the other hand, may not require such harsh actions. For example, if you can silently log an error, pretending that nothing ever happened and allowing the app to continue on as normal, that oftentimes leads to a better user experience.
Let's see how we can implement this in our apps.
- React
- Angular
- Vue
Unfortunately, React is not able to handle thrown errors invisibly to the user when they're thrown within a functional component. As mentioned before, this is ultimately because React functional components are simply functions that do not have an escape mechanism built into them for errors being thrown.
As covered before, Angular is able to ignore all errors thrown outside the constructor
method. However, if an error does occur in the constructor
method, it's challenging (but not impossible) to sidestep. Because of the complexity of the code required to ignore constructor
method errors, we won't cover it in this book.
To avoid an error blanking out your Vue application, simply return false
from your onErrorCaptured
composition.
<!-- App.vue --><script setup>import { onErrorCaptured } from "vue";import Child from "./Child.vue";onErrorCaptured((err, instance, info) => { console.log(err, instance, info); return false;});</script><template> <Child /></template>
This allows components like this:
<!-- Child.vue --><script setup>throw new Error("Test");</script><template> <p>Hello, world!</p></template>
To still render their contents while logging the error.
Fallback UI
While silently failing can be a valid strategy to hide errors from your user, other times, you may want to display a different UI when an error is thrown.
For example, let's build a screen that tells the user that an unknown error has occurred when something is thrown.
- React
- Angular
- Vue
Because our ErrorBoundary
component renders the children that are passed in, we can update our state when an error occurs. To do this, React provides a special static handler method called getDerivedStateFromError
, which allows us to set a property in our state
object when an error is hit.
class ErrorBoundary extends Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { console.log(error, errorInfo); } render() { if (this.state.hasError) { return <h1>Something went wrong.</h1>; } return this.props.children; }}
Because a custom error handler is implemented using an Angular service, we can use our inject
function to gain access to the error handler.
From there, it's as simple as storing a Boolean when an error is thrown and using that Boolean to render out a fallback UI when true
.
import { Component, inject, ErrorHandler, OnInit } from "@angular/core";class MyErrorHandler implements ErrorHandler { hadError = false; handleError(error: unknown) { console.log(error); this.hadError = true; }}@Component({ selector: "app-root", standalone: true, imports: [ChildComponent], template: ` @if (errorHandler.hadError) { <h1>There was an error</h1> } @if (!errorHandler.hadError) { <child-comp /> } `,})class AppComponent { errorHandler = inject(ErrorHandler) as MyErrorHandler;}// Provide the error handler
Unlike most instances of
inject
usage, we have to useas MyErrorHandler
, otherwise TypeScript does not know about the newhadError
property we just set.
Despite this code working well to display a fallback UI in most error throwing scenarios, the
ErrorHandler
service can't prevent the rendering error caused byconstructor
errors. As a result, if you want to display a UI fallback for those errors, you'll need to either implement a somewhat complex bit of code or wait for Angular to implement the functionality itself.
Because we still have full access to our component's state within onErrorCaptured
, we can change a ref
from false
to true
to keep track of if an error occurred.
If it hasn't rendered our main app, otherwise render our fallback UI.
<!-- App.vue --><script setup>import { onErrorCaptured, ref } from "vue";import Child from "./Child.vue";const hadError = ref(false);onErrorCaptured((err, instance, info) => { console.log(err, instance, info); hadError.value = true; return false;});</script><template> <p v-if="hadError">An error occurred</p> <Child v-if="!hadError" /></template>
Displaying the Error
While displaying a fallback UI is often to the user's benefit, most users want some indication of what went wrong rather than simply that "something" went wrong.
Let's display the error thrown by the component to our users.
- React
- Angular
- Vue
While we previously used getDerivedStateFromError
to set a Boolean in our state
object, we can instead use the first argument of the static handler to assign the object to an Error
value.
// JSON.stringify-ing an Error object provides `{}`.// This function fixes thatconst getErrorString = (err) => JSON.stringify(err, Object.getOwnPropertyNames(err));class ErrorBoundary extends Component { state = { error: null }; static getDerivedStateFromError(error) { return { error: error }; } componentDidCatch(error, errorInfo) { console.log(error, errorInfo); } render() { if (this.state.error) { return ( <div> <h1>You got an error:</h1> <pre style={{ whiteSpace: "pre-wrap" }}> <code>{getErrorString(this.state.error)}</code> </pre> </div> ); } return this.props.children; }}
Rather than storing a Boolean when an error occurs, we can store the error value itself within an error
object and display the underlying value when present.
class MyErrorHandler implements ErrorHandler { error: unknown = null; handleError(error: unknown) { console.log(error); this.error = error; }}@Component({ selector: "app-root", standalone: true, imports: [ChildComponent], template: ` @if (errorHandler.error) { <div> <h1>You got an error:</h1> <pre style="white-space: pre-wrap"><code>{{ errorHandler.error }}</code></pre> </div> } @if (!errorHandler.error) { <child-comp /> } `,})class AppComponent { errorHandler = inject(ErrorHandler) as MyErrorHandler;}// ...
Using our ref
to keep track of the error, we can then display the error's contents on-screen when it occurs.
<!-- App.vue --><script setup>import { onErrorCaptured, ref } from "vue";import Child from "./Child.vue";const error = ref(null);onErrorCaptured((err, instance, info) => { console.log(err, instance, info); error.value = err; return false;});// JSON.stringify-ing an Error object provides `{}`.// This function fixes thatconst getErrorString = (err) => JSON.stringify(err, Object.getOwnPropertyNames(err));</script><template> <div v-if="error"> <h1>You got an error:</h1> <pre style="white-space: pre-wrap" ><code>{{ getErrorString(error) }}</code></pre> </div> <Child v-if="!error" /></template>
If you bind
{{error}}
rather than{{error.message}}
, you'll end up with the error:Uncaught (in promise) RangeError: Maximum call stack size exceeded
As Vue's internals attempt to wrap and unwrap the Error object in its reactivity handler.
Challenge
Let's say that we were building out our previous code challenge and accidentally typo-d the name of a variable in our Sidebar
component:
- React
- Angular
- Vue
const Sidebar = forwardRef(({ toggle }, ref) => { const [isCollapsed, setIsCollapsed] = useState(false); const setAndToggle = (v) => { setIsCollapsed(v); toggle(v); }; // ... const toggleCollapsed = () => { setAndToggle(isCollapsed); }; /** * `collapsed` doesn't exist! * It's supposed to be `isCollapsed`! 😱 */ if (collapsed) { return <button onClick={toggleCollapsed}>Toggle</button>; } return ( <div> <button onClick={toggleCollapsed}>Toggle</button> <ul style={{ padding: "1rem" }}> <li>List item 1</li> <li>List item 2</li> <li>List item 3</li> </ul> </div> );});
Upon rendering the sidebar, we're greeted with a JavaScript ReferenceError
:
"collapsed is not defined";
@Component({ selector: "app-sidebar", standalone: true, template: ` <!-- "isCollapsed" is a boolean! --> <!-- It's supposed to be "toggleCollapsed"! 😱 --> @if (isCollapsed) { <button (click)="collapsed()">Toggle</button> } @if (!isCollapsed) { <div> <button (click)="collapsed()">Toggle</button> <ul style="padding: 1rem"> <li>List item 1</li> <li>List item 2</li> <li>List item 3</li> </ul> </div> } `,})class SidebarComponent { @Output() toggle = new EventEmitter<boolean>(); // Notice the type cast to `any` isCollapsed: any = false; setAndToggle(v: boolean) { this.isCollapsed = v; this.toggle.emit(v); } // ... toggleCollapsed() { this.setAndToggle(!this.isCollapsed); }}
You'll notice that we had to use TypeScript to tell our
isCollapsed
is typeany
. Without this change, TypeScript will correctly flag an error during build time that:This expression is not callable. Type 'Boolean' has no call signatures.
Upon clicking the sidebar toggle, we're greeted with a JavaScript TypeError
:
"Error: ctx_r4.isCollapsed is not a function";
<!-- Sidebar.vue --><script setup>import { ref } from "vue";const emits = defineEmits(["toggle"]);const isCollapsed = ref(false);const setAndToggle = (v) => { isCollapsed.value = v; emits("toggle", v);};// ...const toggleCollapsed = () => { setAndToggle(!isCollapsed.value);};</script><template> <!--`collapsed` doesn't exist!--> <!--It's supposed to be `isCollapsed`! 😱--> <button v-if="isCollapsed" @click="collapsed()">Toggle</button> <div v-if="!isCollapsed"> <button @click="collapsed()">Toggle</button> <ul style="padding: 1rem"> <li>List item 1</li> <li>List item 2</li> <li>List item 3</li> </ul> </div></template>
Upon clicking the sidebar toggle, we're greeted with a JavaScript TypeError
:
"Uncaught TypeError: _ctx.collapsed is not a function";
While we can solve this by correcting the typo, we'll also want to add an error handler to log these kinds of issues in case they happen in production. After all, if a bug is found in the wild without a way to report it back to the developer, is it ever fixed?
Let's solve this by:
- Figuring out how the user will report bugs
- Implementing an error handler
- Showing the user a nicer error screen
Reporting Bugs Back to Developers
Let's provide the user a means to email us if they find something similar in their time using the app.
We can do this by showing the user a mailto:
link when an error occurs. That way, reporting the bug is a single mouse click.
This mailto:
link might look like the following HTML
<a href="mailto:dev@example.com&subject=Bug%20Found&body=There%20was%20an%20error" >Email Us</a>
Where subject
and body
are encoded using encodeURIComponent
like so:
// JavaScript pseudo-codeconst mailTo = "dev@example.com";const errorMessage = `There was some error that occurred. It's unclear why that happened.`;const header = "Bug Found";const encodedErr = encodeURIComponent(errorMessage);const encodedHeader = encodeURIComponent(header);const href = `mailto:${mailTo}&subject=${encodedHeader}&body=${encodedErr}`;// HREF can be bound via each frameworks' attribute binding syntaxconst html = `<a href="${href}">Email Us</a>`;
Implementing the Error Handler
With a plan of attack outlined, let's take a step back and evaluate how we'll implement our error handler in the first place.
Remember: Implement first, increment second.
Let's start with an error handler that will catch our error and display a UI when it occurs.
We'll also make sure that this error handler is application-wide to ensure that it shows up when any error in the app comes up:
- React
- Angular
- Vue
class ErrorBoundary extends Component { state = { error: null }; static getDerivedStateFromError(error) { return { error: error }; } render() { const err = this.state.error; if (err) { return ( <div> <h1>There was an error</h1> <pre> <code>{err.message}</code> </pre> </div> ); } return this.props.children; }}const Root = () => { return ( <ErrorBoundary> <App /> </ErrorBoundary> );};
class MyErrorHandler implements ErrorHandler { error: any = null; handleError(error: unknown) { console.log(error); this.error = error; }}@Component({ selector: "error-catcher", standalone: true, template: ` @if (errorHandler.error) { <div> <h1>You got an error:</h1> <pre style="white-space: pre-wrap"><code>{{ errorHandler.error }}</code></pre> </div> } @if (!errorHandler.error) { <ng-content></ng-content> } `,})class ErrorCatcher { errorHandler = inject(ErrorHandler) as MyErrorHandler;}@Component({ selector: "app-root", standalone: true, imports: [LayoutComponent, SidebarComponent, ErrorCatcher], template: ` <error-catcher> <!-- The rest of the app --> </error-catcher> `,})class AppComponent implements OnInit, OnDestroy { // ....}bootstrapApplication(AppComponent, { providers: [{ provide: ErrorHandler, useClass: MyErrorHandler }],});
<!-- ErrorCatcher.vue --><script setup>import { onErrorCaptured, ref } from "vue";const error = ref(null);onErrorCaptured((err, instance, info) => { error.value = err; return false;});</script><template> <div v-if="error"> <h1>There was an error</h1> <pre><code>{{error.message}}</code></pre> </div> <slot v-if="!error" /></template>
<!-- App.vue --><script setup>import ErrorCatcher from "./ErrorCatcher.vue";// ...</script><template> <ErrorCatcher> <!-- The rest of the app --> </ErrorCatcher></template>
Showing a Nicer Error Message
Now that we have a method of showing the error to the user when it occurs, let's make sure that we can report the bug back to the development team. We'll do this by displaying all the information a user would need to report a bug alongside an auto-filled `mailto:`` link so that emailing the developer is a single-button press away.
- React
- Angular
- Vue
class ErrorBoundary extends Component { state = { error: null }; static getDerivedStateFromError(error) { return { error: error }; } render() { const err = this.state.error; if (err) { const mailTo = "dev@example.com"; const header = "Bug Found"; const message = ` There was a bug found of type: "${err.name}". The message was: "${err.message}". The stack trace is: """ ${err.stack} """ `.trim(); const encodedMsg = encodeURIComponent(message); const encodedHeader = encodeURIComponent(header); const href = `mailto:${mailTo}&subject=${encodedHeader}&body=${encodedMsg}`; return ( <div> <h1>{err.name}</h1> <pre> <code>{err.message}</code> </pre> <a href={href}>Email us to report the bug</a> <br /> <br /> <details> <summary>Error stack</summary> <pre> <code>{err.stack}</code> </pre> </details> </div> ); } return this.props.children; }}
Final code output
@Pipe({ name: "errorHref", standalone: true })class ErrorHrefPipe implements PipeTransform { transform(err: Error | null): string { console.log({ err }); if (!err) return ""; const mailTo = "dev@example.com"; const header = "Bug Found"; const message = ` There was a bug found of type: "${err.name}". The message was: "${err.message}". The stack trace is: """ ${err.stack} """ `.trim(); const encodedMsg = encodeURIComponent(message); const encodedHeader = encodeURIComponent(header); const href = `mailto:${mailTo}&subject=${encodedHeader}&body=${encodedMsg}`; return href; }}@Component({ selector: "error-catcher", standalone: true, imports: [ErrorHrefPipe], template: ` @if (errorHandler.error) { <div> <h1>{{ errorHandler.error.name }}</h1> <pre style="white-space: pre-wrap" ><code>{{ errorHandler.error.message }}</code></pre> <a [href]="errorHandler.error | errorHref">Email us to report the bug</a> <br /> <br /> <details> <summary>Error stack</summary> <pre style="white-space: pre-wrap" ><code>{{ errorHandler.error.stack }}</code></pre> </details> </div> } @if (!errorHandler.error) { <ng-content></ng-content> } `,})class ErrorCatcher { errorHandler = inject(ErrorHandler) as MyErrorHandler;}
Final code output
<script setup>import { onErrorCaptured, ref, computed } from "vue";const error = ref(null);const mailTo = "dev@example.com";const header = "Bug Found";const message = computed(() => !error.value ? "" : ` There was a bug found of type: "${error.value.name}". The message was: "${error.value.message}". The stack trace is: """ ${error.value.stack} """ `.trim(),);const encodedMsg = computed(() => encodeURIComponent(message.value));const encodedHeader = encodeURIComponent(header);const href = computed( () => `mailto:${mailTo}&subject=${encodedHeader}&body=${encodedMsg.value}`,);onErrorCaptured((err, instance, info) => { error.value = err; return false;});</script><template> <div v-if="error"> <h1>{{ error.name }}</h1> <pre><code>{{error.message}}</code></pre> <a :href="href">Email us to report the bug</a> <br /> <br /> <details> <summary>Error stack</summary> <pre><code>{{error.stack}}</code></pre> </details> </div> <slot v-if="!error" /></template>