Everything Frontend

Errors in JavaScript

Largely due to its complicated history and implicit coercion in many places JavaScript has a lot of inconsistencies when dealing with how errors are reported from the functions and processed by the client code.

In this article, I will try to describe some of the existing strategies and will outline some of the new possibilities that became viable with ECMAScript 2015+ as well as things we can learn from functional programming.

Returning Special Values

One way to deal with the errors is to return a “special” value that is treated as an error by the caller. Array.prototype.indexOf is a very good example of this. The problem is that an API like that makes total sense in languages wherea function can return only one value and only of only one type, like C, but does not bode well in a dynamically typed language like JavaScript. Having -1 as “not found” result is quite unintuitive.

Things are even worse with the Array methods that are returning `undefined` as an error output. Vivid examples of this are pop and find. The problem with both is that if the Array contains undefined value it is impossible to tell if the function succeeded or failed.

One way to deal with it, which goes well with the dynamic typing nature of JavaScript is to return a value of a very specific type or a class. Here’s how pop function will look like in this case:

class ArrayEmptyError {}
function pop (arr) {
  if (arr.length === 0) {
    return new ArrayEmptyError();
  }
  return arr.pop();
}
const element = pop([1, 2, 3], 42);
if (element instanceof ArrayEmptyError) {
  console.error(element)
} else {
  console.log(element);
}
One thing that might make you wonder here is why I'm not inheriting the NotFoundError from built-in Error class. The reason is that just constructing an Error object in many modern VMs creates a stack trace which is a quite expensive operation. Considering that we expect this type of error to be part of the regular flow of the application, this not necessary.

However, besides having the same problem as undefined when Array possibly contains an ArrayEmptyError element inside, I believe it is also might be non-trivial for a VM to optimize a function that may return values of different types, but do not quote me on that.

Errors / Exceptions

In addition to returning a special value, some the APIs, like Array’s sort will throw an Error in “exceptional” circumstances, also it is very much in the eye of the beholder as to what constitutes such a circumstance.

One side effect of this is that JavaScript engines optimize code in such a way that try / catch is virtually “free” when the exceptions do not arise, but is impractical for indicating “non-exceptional” flows.

Throwing is also problematic from the typing perspective as neither TypeScript nor Flow support explicit annotations that function may throw. This means that you can't be sure that the function you are calling won't explode spectacularly at runtime.

It is worth noting that with the introduction of await, try / catch is considered to be the canonical way to deal with errors:

async function hello () {
  let name;
  try {
    name = await fetchUserName();
  } catch (e) {
    name = "world";
  }
  return `Hello, ${name}!`;
}

Depending on the particular implementations of the saving and restoring of execution context in a VM, broad adoption of try / catch may result in improved performance for the catch branch allowing us to use it for “non-exceptional” situations.

Multiple Return Values

Returning a couple of values is a weapon of choice when dealing with the errors in Go. This was also always a viable approach in JavaScript as well, but with the introduction of destructuring, it also does not look that bad:

class NotFoundError {}
function indexOf (haystack, needle) {
  const index = haystack.indexOf(needle);
  const error = index === -1 ? new NotFoundError() : null;
  return [index, error];
}
const [index, err] = indexOf([1, 2, 3], 42);
if (err instanceof NotFoundError) {
  console.error(err)
} else {
  console.log('found at index ', index);
}

As far as the benefits of the approach—it is straightforward to implement and the error branch of the code does not introduce a lot of overhead compared to the success branch. The code is also pretty easy to understand and can support an arbitrary number of different errors with any extra information you might want to report.

On the contrary side, at least at the moment VMs do not seem to optimize returning and immediate destructuring of a tuple, so you do create a bunch of throw-away arrays, and if you use it in a tight loop, it might not be great. Although, as always with performance—profile your code before making a decision.

Another problem that you might hear about this style of error handling and that is one of the predominant ones from the critics of the Go language is it is too easy to ignore the error:

const [index] = indexOf([1, 2, 3], 42);
console.log('found at index ', index);

This approach is also not without problems in the TypeScript land as well. While it is possible to enforce type checking of the errors through the usage of discriminated unions and objects, you can not destructure them right away. So you eliminate one of the critique points but loose on the conciseness:

class NotFoundError { }

type IndexOfResult =
  | { index: number }
  | { error: NotFoundError }

function indexOf(haystack, needle) : IndexOfResult {
  const index = haystack.indexOf(needle);
  return index === -1 ? { error: new NotFoundError() } : { index };
}
const result = indexOf([1, 2, 3], 42);
if ('error' in result) {
  console.error(result.error)
} else {
  console.log(result.index)
}

Functors and Monads

First of all, if you do not know what functors and monads are—don’t worry. I will walk you through some of the widespread examples from JavaScript that follow this approach and show how it can be used for your code. If you do know what they are, but never thought about applying it to your JavaScript code, maybe this section will give you some hints.

Let’s start with functors. In very basic terms, it is just a container that provides a special method (or a standalone function) to do operations on the content of that container. The most well-known example is Array and map:

console.log(['foo', 'bar', 'buzz']
  .map(item => item.length));
// [3, 3, 4]

Another typical example is jQuery. It also maintains an internal collection of the selected DOM nodes and allows you to perform some operations on them:

console.log($('.foo').filter('div').length);

One might ask what do these examples have to do with error handling. The answer lies in the fact that as long as you are working with the methods of the container, you do not care how many items that container has, or it has any at all. This only becomes relevant if you want to extract the contents, which may not even be required depending on the type of processing you are doing.

When you do need to extract the contents, it is useful to talk about monads, and in particular about something many JavaScript developers are quite used to—Promise. While it might not be a valid monad in the mathematical sense, it is close enough for our purposes. You have probably seen code that looks something like this:

someFunctionThatReturnsAPromiseOfAString().then(
  function getIndex(string) {
    const index = string.indexOf('foo');
    if (index === -1) {
      return Promise.reject(new ArrayEmptyError())
    }
    return Promise.resolve(index)
  }
).then(
  function logIndex(index) {
    console.log(index)
  }
).catch(console.error)

What is important to note here, is that the logIndex has no error handling code, it is just concerned with its logic, and the error handling code is also generalized— it does not matter if error came for the original function that provided the promise or first getIndex handler in the chain.

The biggest problem with the monadic code that you might have also observed with promises or async / await is that it is “infectious”—once you get into the chain, all of your logic must reside in it one way or the other. You start with one async operation, and suddenly half of your code is marked as async and having it as such has performance implications as each async function is scheduled separately as a micro-task.

Another issue specifically in JavaScript is that there is no nice syntax ( like Haskell’s do notation) for any monadic operations and while there are libraries that try to make that work, it does require quite some time to get used to, and again, has a performance impact.

Conclusion

None of the approaches I described are perfect. The issue I see with JavaScript is that it uses all of them, and probably some more creative ones. This makes both mental contexts switching quite expensive and often requires additional code to transform one type of errors to another. Finally, the safest options listed above are also the ones that end up looking the ugliest or have poor adoption, which means people often opt into inferior error handling.

Since it is always very easy to critique without providing an alternative or at least a bit of advice, here are my thoughts on this.

For your projects, it might be worth to adopt a particular style of error handling and stick to it. If a dependency or a standard library code does not conform to that style, it is usually possible to wrap the functionality you need. The additional wrapping code is often compensated by the common error handling code that you might extract from your functions.

From the standard library perspective, since we now have modules it would be great to see an updated standard library with normalized names, error handling and less type coercion available as a namespace, something like this:

import {NotFoundError, indexOf} from 'ecma2020/Iterable';
const { index, error } = indexOf('foobar', 'buzz');
if (error instanceof NotFoundError) {
  console.error(error)
} else {
  console.log(index)
}