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:
1// NOTE: using typescript syntax here in order to show what types the arguments are
2
3function onError(
4 msg: string | Event,
5 source?: string,
6 lineno?: number,
7 colno?: number,
8 error?: Error
9) {
10 // error handling code here!
11}
12
13window.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.
1window.addEventListener("error", function (event) {
2 // error handling code here!
3});
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.
1try {
2 throw new Error("I'm broken");
3 // generates an exception
4} catch (e) {
5 // statements to handle any exceptions
6} finally {
7 // clean up
8}
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 Promise
s.
Promise
s 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
.
1// You can catch errors asynchronously by listening to Promises...
2asyncActionThatReturnsAPromise().catch((error) => appsignal.sendError(error));
Errors can also be caught in the onRejected
handler, a second parameter to .then
that takes a function.
1asyncActionThatReturnsAPromise().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 Promise
s, you do yourself and any future developers using your code the courtesy to reject Promise
s with Error
objects.
Any Promise
s 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
.
1window.onunhandledrejection = function (e) {
2 // error handling code here!
3};
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!
1// ...or by using async/await
2async function() {
3 try {
4 const result = await asyncActionThatReturnsAPromise();
5 } catch (error) {
6 appsignal.sendError(error);
7 // handle the error
8 }
9}
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.