Exception safety in JS world

Ron Lau
7 min readSep 3, 2020

--

Exception happens!

As recent versions of JavaScript (formally EMCAScript) provide more programmatic facilities (and syntactic sugar) for us programmers to utilize, code in JavaScript has never been more expressive, though new pitfalls come along. With the advancement of tools in the ecosystem, we are enabled to avoid many of these problems, and write cleaner and clearer code. We have ESLint and its family of plugins to enforce consistent coding standard, and find common problems with static analysis; TypeScript, Flow, and other static type checkers to ensure type safety; Babel to transpile modern and cutting-edge ES syntaxes into syntax compatible with your target.

However, there is a “thing” that plagues us from the beginning of time¹, and even with the tools we have today, still needs to be manually catered: exception.

Ordinary functions may throw an exceptions. Class constructors may throw exceptions. Class methods may throw exceptions. Generators may throw exceptions. Awaiting rejected Promises would results in exceptions thrown. Almost everything may throw an exception. Static analysis doesn't help much in this case, for tracing all possible places that would throw an exception may get every line of your codebase flagged. TypeScript doesn't help too, as exception specification is not a part of a function declaration.

What can we do then?

Point of exception

Before we delve into more details, let’s play a good old game of “spotting the exception”.

Can you spot where an exception may be thrown in the following TypeScript files? Let’s consider Car is a black-boxed class.

Introduce the concept of exception safety

Exception safety², a concept commonly associated with C++, is a set of properties that indicate what would happen when an exception arises in a function, and let user of the function reason about how to handle the exception properly.

Generally speaking, exception safety guarantees are divided into 4 levels, in ascending level of safety:

There is no guarantees what would happen when an exception is thrown. There may be (incomplete) side-effects due to interrupted execution, and you cannot be sure whether the program/internal state is coherent.

There may be side-effects due to partial execution, however, all invariants are preserved, and there is no resource leak. Some program/internal states may be different from that of before the execution, but the all the values are valid.

It is guaranteed to have no side-effect if an exception is thrown. All program/internal states are intact in such case. In other words, the execution either fully completes, or fails as if it is not invoked at all. Similar to an atomic database transaction.

It is guaranteed that the function would always succeed, and never throw any exception.

No exception safety is considered a bug in a program, as it implies the program is incorrect if an exception got thrown.

That’s it; there is no lower level. A failure to meet at least the basic guarantee is always a program bug. Correct programs meet at least the basic guarantee for all functions. [Sutter04]

“Doesn’t that just mean wrapping all code in try-catch block?”

Unfortunately, no. The program flow is still interrupted within a try-catch block when an exception is thrown. You may have prevented an exception from escaping your function, however this by itself still doesn't mean anything meaningful in exception safety. The main point is, as mentioned above, to provide a guarantee that what would happen when an exception is thrown.

For example, using a function-wide try-catch block may make your function "never throw any exception", but you have to guarantee the function "would always succeed" too to be qualify for "no-throw guarantee".

This implies that achieving exception safety is never simply a matter of sprinkling try-catch blocks (or catch handler for promises), but something that is part of the design.

Never make exception safety an afterthought. Exception safety affects a class’s design. It is never “just an implementation detail.” [Sutter99]

Where are exceptions thrown and when is it unsafe?

Let’s go back to inspect the code snippets function by function, and see where the exceptions are thrown.

carHelpers.ts

await fetch(...) on L3 may throw, for example when there is a network error, or when the condition on L5 is true.
Does this break exception safety? No, because no internal state is modified, even if an exception is thrown.

new Car(...) on L13 may throw.
Does this break exception safety? No, because again no internal state is modified even if an exception is thrown. carsCache is reassigned only when rawCars.map(...) completes without throwing an exception.

Is the function exception safe? Yes, the function has “strong exception safety guarantee”.

await getCars() on L6 may throw, just as we illustrated above.
Does this break exception safety? No. No internal state is changed by this point, and getCars offers strong exception safety guarantee.

car.plateNumber on L10 may throw. It may be a surprise to you, but car.plateNumber may be implemented as a getter function, which may throw an exception.
Does this break exception safety? Yes. carByPlateNumberMap would be partially modified if an exception is thrown in the middle of the loop.

Is the function exception safe? No, internal states would be incoherent when an exception is thrown on L10. This is an example where having a try-catch block in place still doesn't make a function exception safe.

Is the function exception safe? Yes, as no exception is thrown in this function. This function is said to provide “no-throw guarantee”.

await getCarByPlateNumberMap() may throw on L4, as illustrated above.
Does this break exception safety? Yes, for getCarByPlateNumberMap not being exception safe. If we assume for a moment that getCarByPlateNumberMap is exception safe, then this function is safe too, as no internal state is modified in this function itself.

Is the function exception safe? No. If getCarByPlateNumberMap is exception safe, this function would be exception neutral: thrown exceptions propagates up to the caller verbatim, and the internal states are intact should that happens.

Is the function exception safe? Yes, and it provides “no-throw exception guarantee”.

index.ts

await getCarByPlateNumber(...) may throw on L7.
Does this break exception safety? Yes, as getCarByPlateNumber is not exception safe.

Assignment to car.owner may throw on L8, as car.owner may have a setter function which may throw an exception.
Does this break exception safety? No. Internal states are intact.

Assignment to car.plateNumber may throw on L9, also due to potential setter implementation.
Does this break exception safety? Yes and no. Nonetheless, car.owner is modified at this point. If the internal state of car is still consistent (no invariant is broken), we may consider this program has "basic exception safety guarantee", otherwise, this is exception unsafe.

Techniques for exception safety

Narrow down the responsibilities of a function

A function having multiple distinct responsibilities is hard to write correctly. Consider breaking such a function into multiple functions, spreading the responsibilities.

Remember that “error-unsafe” and “poor design” go hand in hand: If it is difficult to make a piece of code satisfy even the basic guarantee, that almost always is a signal of its poor design. For example, a function having multiple unrelated responsibilities is difficult to make error-safe. [Sutter04]

“Clone-and-assign”

Clone the object/value/program state you are going to mutate (or create a new instance), operate on the copy, and replace the old value.

Let’s take getCarByPlateNumberMap as an example:

Make good use of the built-in non-mutating functions

ES5 provides a number of non-mutating functions, such as Array.prototype.filter, Array.prototype.map, and Array.prototype.reduce⁴. Array.prototype.slice was provided back from ES3⁵. These functions return a new instance instead of modifying the original object.

Again let’s take getCarByPlateNumberMap as an example. With Array.prototype.map, we can eliminate the for-of-loop too:

Both implementations would provide “strong exception safety guarantee”.

Functional programming library / immutable data structure library

Data immutability is a core concept of functional programming. There are libraries in JS that provides functional programming-like experience, for example, Bacon.js, fp-ts (TypeScript only), and Ramda. For immutable data structures, Immutable.js, and Immer are two popular libraries providing the facilities.

Give the strongest safety guarantee that won’t penalize callers who don’t need it [Sutter04]

It is not a must to always provide strong safety guarantee. Sometimes it is even impossible to do so.

When providing strong safety guarantee is difficult, or it would impose unwanted overhead to those who do not need it, consider providing basic safety guarantee. Basic safety guarantee is the least you should always provide.

Conclusion

Exception is a thing we cannot avoid when programming in JavaScript (and other languages). Handling exceptions correctly and being exception-concious (knows where the exceptions may be thrown) are essential to write correct code. The concept exception safety helps you, and the users of the code you write reason about the implication of an exception. Always aim for strongest safety guarantee that won’t penalize callers who don’t need it, while always provide at least the basic guarantee.

Thank you very much for reading.

[1]: Precisely from EMCAScript 3rd edition, released in December 1999 and came into public use with the release of Internet Explorer 5.5 in July 2020, in which throw and try...catch were introduced into the language. If we count "runtime errors" generated by built-in functions³ that cannot be handled, it would be probably from the very first version of JavaScript.

[2]: First introduced by David Arbrahams in Exception Safety in STLport in 1996, and expanded in Lessons Learned from Specifying Exception-Safety for the C++ Standard Library.

[3]: Section 5.2, P.8, EMCA-262 1st edition

[4]: Section 15.4.4.19–15.4.4.21, P.135–138, EMCA-262 5th edition

[5]: Section 15.4.4.10, P.93, EMCA-262 3rd edition

[Sutter99]: Herb Sutter, Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions. Addison-Wesley, 1999.

[Sutter04]: Herb Sutter and Andrei Alexandrescu, C++ Coding Standards: 101 Rules, Guidelines, and Best Practices. Addison-Wesley, 2004.

--

--