Fixing JavaScript Error Handling

I end up explaining this each time I train a new hire, so I figured I’d write it all out here. For our purposes, error handling breaks down into a) detecting that something has gone wrong, and b) choosing to do the correct thing in response. This is difficult enough to be its own area of research in software engineering, and JavaScript has more difficulty than most languages with error handling. We’re going to focus more on figuring out when things have gone wrong. Let’s start with how this has been done successfully in the past.

Error Codes

A common error detection pattern in the C programming language is to return an integer error code from any function which is potentially unable to complete its task.  By convention a return value of zero means that your function has succeeded, while non-zero results represent specific errors.  Users of your function are expected to notice that you’re returning an error code and deal appropriately with it.  This is reasonably effective and is used in lots and lots of good software.  Here’s an example in C:

bakery_error_t
bake_pie(pie_flavour_t flavour) {
	if (flavour != PIE_CHERRY) {
		return ERR_ALL_I_HAVE_IS_CHERRIES;
	}
	pie_ingredients_t ingredients = get_pie_ingredients(flavour);
	bakery_error_t oven_result = put_ingredients_in_oven(ingredients);
	if (oven_result != ERR_NO_ERROR) {
		return ERR_BAKER_IS_AN_IDIOT;
	}
	return ERR_NO_ERROR;
}

This piece of C code is synchronous, meaning that it’s going to go get the ingredients and attempt to actually finish baking us a pie before returning from this function. It has to finish before returning an error code because we won’t know if anything went wrong until it’s done.

The JavaScript we write can’t be synchronous, because it’s running in an event queue that can only do one thing at a time. If you block this event queue, nothing else happens until you’ve finished. By nothing else, I mean that you’ll completely lock up your browser or server, so getting out of the way as quickly as possible is fantastically important. So we need to bake our pies asynchronously.

Asynchronous Error Codes

Many functional programmers rely on a technique called continuation passing to write straightforward asynchronous code; let’s look at our last example translated fairly literally to asynchronous JavaScript written in continuation passing style.

function bake_pie(flavour, callback) {
	if (flavour !== PIE_CHERRY) {
		return setTimeout(callback, 0, ERR_ALL_I_HAVE_IS_CHERRIES);
	}
	get_pie_ingredients(flavour, got_ingredients);
	function got_ingredients(ingredients) {
		put_ingredients_in_oven(ingredients, finished_baking);
	}
	function finished_baking(oven_result) {
		if (oven_result !== ERR_NO_ERROR) {
			return setTimeout(callback, 0, ERR_BAKER_IS_AN_IDIOT);
		}
		setTimeout(callback, 0, ERR_NO_ERROR);
	}
}

We’re using a built in function called setTimeout, which adds a function on to the event queue. The first argument to setTimeout is the function to add, the second is the approximate number of milliseconds to wait before executing it, and any extra arguments to setTimeout are passed in to the function you added when it’s executed. So, in our example, if someone wants a pie other than cherry, we call callback with our “nothing but cherries” error as soon as everything else on the event queue which is lined up to happen right now has finished.

Notice that we’re referring to got_ingredients on the line before it’s even defined. That looks super weird, but is perfectly legal because of the way functions are made available to their containing scope. We’re doing it this way because we’d like to write asynchronous JavaScript which looks nearly identical to our C code. This is just a personal preference, with the rationale that the visual and logical flow of a function is mostly unchanged despite it now being asynchronous. The difficulty is that we could mistakenly refer to a variable before it’s initialized, so it requires extra care on our part to write it this way.

All that aside, though, that looks fine. Let’s use error codes! Nothing could possib-

Exceptions

The reason that software written in C is able to get away with using error codes is that C doesn’t have an exception mechanism. Exceptions are a way for a program to spontaneously alter its execution flow. Usually, this is accomplished by unwinding our call stack until we encounter a piece of exception handling code, most of the time using the try/catch mechanism. Languages like Java, C++, C# etc. rely on exceptions as a primary method of error handling. Here’s a safe(-ish) extension of our previous example when it’s written in C++:

bakery_error_t
bake_pie_in_cplusplus(pie_flavour_t flavour) {
	try {
		return bake_pie(flavour);
	} catch (bakery_error_t error_code) {
		return error_code;
	} catch (...) {
		return ERR_CAUGHT_EXCEPTION;
	}
}

This should let us deal with any internal exceptions while exposing error codes to the users of our function. It’s an okay approach when coding in C++, since it doesn’t force the complexity of dealing with exceptions onto the calling code. Making sure that your code always functions properly in the presence of exceptions (i.e. writing exception safe code) is reasonably difficult in C++, and nearly impossible in Java. But that’s a whole other article.

Asynchronous Exceptions

JavaScript, to our growing dismay, has exceptions, and there isn’t anything we can do about it. The part that makes this extra wonderful is that our stack is not maintained between functions on the event queue. So, for example, sometime after we’ve called get_pie_ingredients and before we’ve entered got_ingredients our entire stack has been lost. If got_ingredients were to throw an exception, bake_pie wouldn’t be above it on the stack. We can’t use the method we used for C++ here, because our stack keeps going away. Minimally, given that any of the functions we rely on might throw an exception at any point, we need to write this code:

function bake_pie(flavour, callback) {
	if (flavour !== PIE_CHERRY) {
		return setTimeout(callback, 0, ERR_ALL_I_HAVE_IS_CHERRIES);
	}
	try {
		get_pie_ingredients(flavour, got_ingredients);
	} catch (e) {
		return setTimeout(callback, 0, ERR_CAUGHT_EXCEPTION);
	}
	function got_ingredients(ingredients) {
		try {
			put_ingredients_in_oven(ingredients, finished_baking);
		} catch (e) {
			return setTimeout(callback, 0, ERR_CAUGHT_EXCEPTION);
		}
	}
	function finished_baking(oven_result) {
		if (oven_result !== ERR_NO_ERROR) {
			return setTimeout(callback, 0, ERR_BAKER_IS_AN_IDIOT);
		}
		setTimeout(callback, 0, ERR_NO_ERROR);
	}
}

(We could additionally check whether the caught exception was really an error code, and call our callback with that.) Unfortunately this still isn’t correct; what if put_ingredients_in_oven added finished_baking to the event queue and then threw an exception? It would queue a call to our callback with a “caught exception” error code. Our finished_baking function would then be called, and it will queue up another call to our callback. Execution will continue from our callback twice, and our application could very easily be broken with no way for us to fix it.

That sucks, but the issue isn’t unique to exceptions. Nothing prevents put_ingredients_in_oven, for example, from calling finished_baking a hundred times. What we need is a way of preventing a function from being called more than once. A function whose effect isn’t changed by multiple invocations is called idempotent. We can write a helper function to turn any function into one of these, and use it for our continuations so that they can only be called once.

function once(callback) {
	function empty() {}
	return function() {
		callback.apply(this, arguments);
		callback = empty;
	}
}

Here, we’re capturing our callback and an empty “do nothing” function in a closure, which will invoke the callback function the first time it’s called, and immediately overwrite its reference with our do nothing function. The way we’ve written it has the interesting additional property that we can’t get at the return value of our callback, which encourages people to stick to continuations.

So at this point we’re able to prevent a function from being called twice. We also need to stop execution of subsequent continuations after our callback has been called. One way to do this is to set a flag and check it inside each continuation to see if we’re finished. We can get rid of some of the verbosity and capacity for error by baking this into our idempotent callback function. Our code might end up looking something like this:

function bake_pie(flavour, unsafe_callback) {
		var is_finished = false;
	var idempotent_callback = once(unsafe_callback);
	function done(error_code) {
		is_finished = true;
		setTimeout(idempotent_callback, 0, error_code);
	}
	if (flavour !== PIE_CHERRY) {
		return done(ERR_ALL_I_HAVE_IS_CHERRIES);
	}
	try {
		get_pie_ingredients(flavour, once(got_ingredients));
	} catch (e) {
		return done(ERR_CAUGHT_EXCEPTION);
	}
	function got_ingredients(ingredients) {
		if (is_finished) { return; }
		try {
			put_ingredients_in_oven(ingredients, once(finished_baking));
		} catch (e) {
			return done(ERR_CAUGHT_EXCEPTION);
		}
	}
	function finished_baking(oven_result) {
		if (is_finished) { return; }
		if (oven_result !== ERR_NO_ERROR) {
			return done(ERR_BAKER_IS_AN_IDIOT);
		}
		return done(ERR_NO_ERROR);
	}
}

Note that we’ve wrapped all of our continuations with once and replaced all of our setTimeout calls with done. The first time someone calls our done closure, it queues an invocation of the idempotent callback and sets an early exit flag which each continuation has been modified to check before it’s executed. We could get more clever and write a checked function which would check is_finished for us and not execute a continuation if it was true, but I don’t want to do that so I’m leaving it as an exercise for the reader. At some point your helpful abstractions end up obfuscating what’s actually happening, which is incredibly dangerous in JavaScript.

So, while this code works, the entire situation is kind of unfortunate. We’re attempting to write reliable asynchronous software, and it’s not nearly as straightforward as the C example. This code is a lot more fragile and a lot more verbose than our original synchronous code. It’s also much slower. JavaScript code in most modern browsers (and node.js) is Just-In-Time (JIT) compiled, a mechanism by which frequently executed pieces of interpreted code are compiled at runtime in order to speed up subsequent execution. Try/catch statements prevent code from being JIT compiled (my friend Guillaume informs me that I am vastly oversimplifying this, but that the gist is correct). The takeaway is that without try/catch statements, exceptions are nuclear.

So, What’s Important Here?

It’s definitely possible to write robust(-ish) code in JavaScript, as long as we understand the problems that might arise and develop tools to deal with them. In critical portions of our games, we’re occasionally required to make a trade-off between correctness and performance (omitting exception handling code, for example, or not making callbacks idempotent). Our extensive code review, training, and QA processes help us keep our codebase viable in these cases.

Despite requiring more care when dealing with error handling, JavaScript is an extremely expressive language in which our developers are increasingly productive. Moving forward, projects like Dart and Emscripten indicate that safer languages can be used for our future development, hopefully allowing us to simplify the ways we deal with this kind of problem. In the meantime, I’m just enjoying how silly the web’s core technologies are.

Posted in HTML5

Leave a Reply

Your email address will not be published. Required fields are marked *

*