Uncaught errors in Node.js and the browser

JavaScript

When writing code, we can expect a wide range of errors to happen. We should handle it by writing our code with that in mind. Even though that’s the case, some errors might slip through the cracks. This article looks into what happens with unhandled errors both in Node.js and in the browser.

Errors in JavaScript

In web development, many situations can result in an error. In general, we could divide them into two categories.

Programmer errors

Programmer errors are caused by bugs in our code. A common example is trying to access a property of undefined.

Uncaught TypeError: Cannot read property ‘data’ of undefined

Operational errors

We might run into operational errors even if we wrote our code correctly. They don’t represent bugs but signify that something went wrong. A good example is a user sending a request when logging in, but providing a wrong password.

If you want to learn about the very basics of managing errors, check out Handling errors in JavaScript with try…catch and finally

What happens when an exception is thrown

In JavaScript, an error is an object that we can throw to indicate that something went wrong.

The above snippet is based on the code from the API with NestJS series.

A crucial thing to understand is how JavaScript reacts to exceptions being thrown. First, it stops the execution of the code. Then, it moves up through the call stack of our functions, looking for the closest block.

Above, because the function threw an error, we can see in the console.

Uncaught errors in the browser

Let’s look into the MDN documentation for the throw statement:

If no block exists among caller functions, the program will terminate.

There is quite a bit more to it, though. When there is an uncaught error in the browser, it aborts processing the current script or a file. When this happens, the object emits the error event. We can listen to it with .

A legacy way to listen for the error event is to use .

If we attempt to try the above code out, we might run into an issue. The above event does not fire for errors originated from the Developer Tools console. This applies both to Chrome and Firefox.

Listening to the error event allows us to create a last-resort error handling. For example, we could implement a logic that redirects the user to a page that informs that an unexpected error happened.

React embraces this approach with error boundaries. If you want to know more, check out Bug-proofing our application with Error Boundaries and the Strict Mode

We can access the error event in the listener. Aside from accessing its properties, we can call the method. Doing so causes the error not to be seen in the console.

If we throw an error in the above listener, it does not emit the error event. Thanks to that, the JavaScript engine avoids infinite recursion.

Uncaught errors in promises

We should also implement error handling for our promises.

When we don’t catch the above promise rejection, we can expect the error to appear in the console.

Uncaught (in promise) TypeError: Failed to fetch

When this happens, the object emits the event. We can access the PromiseRejectionEvent and prevent the error from appearing in the console.

A legacy way of attach a listener to this event is to use .

There are some edge cases that we should know about. Let’s consider the code below:

Something went wrong
Uncaught (in promise) TypeError: Failed to fetch

Above, we can see that error appears to be uncaught even though we’ve used the function. To understand this, let’s look closer into the then function. It accepts two arguments: a function called when the promise resolves, and the callback used when the promise is rejected. If we don’t explicitly provide the rejection callback, JavaScript does the following:

We don’t catch the error thrown in the callback, and therefore, it appears as uncaught in the console. This issue does not appear if we chain the and functions.

Uncaught errors in Node.js

By default, causing an uncaught exception in Node.js results in printing the stack trace and exiting the process with code . When a process returns a non-zero code, it signifies an error.

With Node.js, we don’t have the object. Even though that’s the case, there are some events related to uncaught exceptions.

By listening to the error, we can alter the default behavior. When we do that, Node.js no longer returns code but still terminates the process. We can restore the exit code through the property.

If we want to listen for uncaught exceptions, but we don’t want to alter the default behavior, we can listen to the event instead.

We can also use to attach the above listener.

Although we could restart the process within the above listener, Node.js documentation strongly advises against it. The listener is a place to perform the synchronous cleanup of allocated resources, such as files. It is not recommended to resume operation after the event was emitted.

If we want to restart a crashed application more reliably, the official documentation suggests using an external monitor. An example of such is PM2.

Uncaught errors in promises

The topic of unhandled promise rejections in Node.js is somewhat tricky. This is because the default behavior changed across the Node.js versions.

Above, I use the function to work with promises instead of callbacks. If you want to know more about the file system module, check out Node.js TypeScript #1. Modules, process arguments, basics of the File System

When running the above code with Node.js version previous to 15, we can see the following:

UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag --unhandled-rejections=strict (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
[DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

This means that the process is not terminated. This behavior has been changed in Node.js 15, and the flag is now set to by default. Because of that, running into an unhandled promise rejection terminates the process if we don’t listen to the event.

We can still set to if we need the previous behavior.

Node.js emits the event every time a promise is rejected without error handling. By listening to it, we can keep track of rejected promises and implement logging, for example.

If we set to , our process terminates on uncaught promise rejections even if we listen to the event.

Summary

In this article, we’ve looked into uncaught errors in JavaScript. It turns out, the browsers and Node.js handle it differently. A topic worth looking into more was unhandled promise rejections. The behavior differs not only between browsers and Node.js but also across different Node.js versions. All this knowledge can come in handy when debugging and building logging functionalities.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments