javascript

JavaScript Errors: An Exceptional History - Part II

Adam Yeats

Adam Yeats on

JavaScript Errors: An Exceptional History - Part II

Hello again! Welcome to the finalé of a two-part series of posts on errors in JavaScript.

Last time, we took a look into the history of errors in JavaScript — how JavaScript shipped without runtime exceptions, how error handling mechanisms were later added both to the fledgeling web browsers of the day and to the ECMAScript spec, and how they future efforts to standardise these features would be connected to the politics of the browser wars of the late 90's and 2000's.

This time, we'll focus a little more on the state of affairs in JavaScript today. We'll look at the different ways you can handle errors in your app today, the various idiosyncrasies they have, and how you can use our JavaScript client library to report errors from your app to our dashboard.

Let's do it!

Handling Errors Today

After the last post, you may be forgiven for thinking that handling errors gracefully in JavaScript might be a bit of a nightmare. Luckily, it's not so daunting a prospect as it might seem, but there are quite a few different ways to handle errors with varying levels of scope and different use cases.

window.onerror Handler

The window.onerror handler exists today in all modern web browsers as a means to catch uncaught exceptions from the current window. Any thrown error that is not otherwise handled in a try/catch block will be passed to the handler as the first argument to that function. The current window refers to the current global context, so it's important to note that <iframe>s and Web Workers (for example) will have their own window context.

-> When we use a window in the following examples, we're referring to the global window context of the browser.

By assigning a function to window.onerror, we can write custom logic to handle any uncaught exceptions that are thrown during the lifecycle of our application:

ts
// NOTE: using typescript syntax here in order to show what types the arguments are function onError( msg: string | Event, source?: string, lineno?: number, colno?: number, error?: Error ) { // error handling code here! } window.onerror = onError;

You might notice that some of these arguments are marked as optional. This is because, as you might guess, browsers disagree on the number of arguments passed to the onError handler. Browsers as recent as Safari 9, for example, do not pass an Error object as its fifth argument. Internet Explorer 9 passes neither the colno or error arguments. Because of this inconsistency, care needs to be taken when writing an onError handler that works in older browsers.

However, thanks to the existence of the Error object in most modern browsers, you can normally rely on that 5th argument to be present, which will include some useful information that might come in handy when debugging, such as the current stack trace (error.stack).

-> As this handler tends to be a little noisy (thanks, browser extensions...), the AppSignal JavaScript library doesn't automatically catch exceptions passed to the window.onerror handler. Instead, we have packaged this functionality in an optional plugin — the @appsignal/plugin-window-events package on npm.

As a convenience, once the onError handler is called, most browsers will call console.error behind the scenes to display the Error object (often including its stacktrace) in the console.

The Document Object Model Level 2 specification introduced the EventTarget interface to provide a generic way to bind event listeners to an Element (or other objects like Document and Window) that worked cross-browser, but also added features like the ability to have multiple handlers bound to an event. This means that many of the older event handlers, such as our friend onError, received a modern facelift.

javascript
window.addEventListener("error", function (event) { // error handling code here! });

In this example, you can see that the event of type ErrorEvent is passed as the single argument to your callback. The event object contains both the information about the error but also the event itself, but again, older browsers differ in the information they provide in the event.

try/catch Operator

For synchronous code, the humble try/catch operator remains the most common way to handle exceptions. As we discussed in the previous post, try/catch exception handling allows you to try executing a block of code that may throw errors at runtime; if it does, the exception is then caught by the catch block, allowing us to control what happens and what state our app is left in.

While it's certainly true that JavaScript still allows you to throw any value as an exception, community convention has filled the gap where the ECMAScript specification leaves ambiguity; it is more commonplace to receive Error objects as the argument to the catch block nowadays, and good library implementors will generally throw Error objects for you to handle.

javascript
try { throw new Error("I'm broken"); // generates an exception } catch (e) { // statements to handle any exceptions } finally { // clean up }

In the catch block, you should add any code that allows you to put your app back into a defined state.

React's documentation for their Error Boundaries feature explains the problem well from a UI perspective, and the same is also true for exception handling as a whole:

For example, in a product like Messenger leaving the broken UI visible could lead to somebody sending a message to the wrong person. Similarly, it is worse for a payments app to display the wrong amount than to render nothing.

It's also a good idea to log your exception somewhere — failing silently is rarely useful, your aim here is to surface the exception as best as you can to debug problems before they become a problem for the user.

-> In your code, the catch block is where you would want to include your call to appsignal.sendError() if you're using our JavaScript library. Here, you can pass the Error object as an argument and have it appear in your Errors dashboard.

The finally block tends to not be as useful in JavaScript as it is in other languages. In the finally block, normally should try to clean-up any resources created before the exception was thrown, however as JavaScript is a garbage-collected language and resources are allocated and de-allocated dynamically, we often don't have to think about this much. There are times where this can be useful, however, such as for closing open connections to remote services regardless of whether the request to it was successful or not.

Promises and Async JavaScript

Admittedly, in our last post, we might have seemed a little negative about the design of JavaScript as a language. While it's almost certainly true that a lot of mistakes were made — and thanks to the ever-present need for backwards compatibility, many of them still exist today — arguably, there has been a lot of ground covered since then to make amends, and many aspects of the original design of JavaScript still hold up well today.

One of those areas that JavaScript is great at is asynchronous programming. JavaScript is an event-driven language, which is, in its simplest terms, the means to allow code to be executed by listening for events that can be triggered based on user interaction, or even messages from other programs. This is a great fit for a language like JavaScript that is mostly found embedded in a graphical environment, where you might feasibly want to execute code based on mouse clicks, or key presses.

Thanks to JavaScript's Event Loop (a concept we'll cover in full in a later edition of JavaScript Sorcery) and recent developments in the language, JavaScript lets you define points in your program where the flow of execution can be returned to the program in lieu of a value, allowing the rest of your program to run and the UI to update, and the value to the latter be filled later. We call these values Promises.

Promises themselves can contain exceptions, which when they are thrown, cause the Promise to become rejected. Once rejected, a Promise can execute a user-defined callback that we chain to it using .catch.

javascript
// You can catch errors asynchronously by listening to Promises... asyncActionThatReturnsAPromise().catch((error) => appsignal.sendError(error));

Errors can also be caught in the onRejected handler, a second parameter to .then that takes a function.

javascript
asyncActionThatReturnsAPromise().then(onFulfilled, onRejected):

The first argument to the .catch callback will normally be an Error object, but just like the try/ catch statements above, there is no explicit rule about what kind of value a Promise can be rejected with and thus passed to the .catch callback. It could technically be any value. We recommend that, when writing your own Promises, you do yourself and any future developers using your code the courtesy to reject Promises with Error objects.

Any Promises that become rejected that don't have a callback bound to the .catch handler will instead fire a callback on the window object called onunhandledrejection.

javascript
window.onunhandledrejection = function (e) { // error handling code here! };

Recently, the ECMAScript standard was amended to add the async/await keywords. With these keywords, we can write async code that looks like synchronous code by using the await keyword within an async function to denote to the program that it should pause execution of the async function and wait for a value that a Promise is fulfilled with.

As we can use async/ await and async functions to write code that looks like it's synchronous even though it's not, then it's sensible to expect that we can also use the try/catch statement to handle exceptions within them, and in fact, we can!

javascript
// ...or by using async/await async function() { try { const result = await asyncActionThatReturnsAPromise(); } catch (error) { appsignal.sendError(error); // handle the error } }

C'est tout!

That's all we have for this week!

Don't forget: our JavaScript integration was released recently and we'd love for you to give it a try in your front-end applications and tell us what you think.

If you liked this post, subscribe to our new JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps