It’s 2018 and modern build pipelines have become quite standard across front-end projects. Having a transpiler like Babel to produce browser-agnostic Javascript is the default nowadays so you can write remotely sane Javascript. It can be easy to forget that the transpilation process can result in suboptimal results, where unexpected side effects of the impedance mismatch of one language to another (ES6 -> ES5 for example) can be subtle and easily missed.

To be fair, this is quite understandable given how complicated modern front-end development can be to learn. But it’s important to know about the the edge cases involved in transpilation, and understand the risks in forgetting that ultimately what gets run in the browser is not what you write. We’re going to look at one such edge case quickly in this post.

ES6 Classes and Inheritance

Time for a brief revisiting of ES6 classes. ES6 brought the notion of first class (ha) classes, object lifecycle concepts like constructors, and pseudo-OO inheritance. You can write a class as easily as this:

class MySillyClass {
	constructor(foo) {
    	super();
        this.foo = foo;
    }
}

and make use of it like so:

const mySillyClassInstance = MySillyClass("bar");
console.log(mySillyClassInstance.foo);
// "bar"

Neat! We get clear syntactical sugar for constructors! You can also inherit from existing classes - for example, extending the standard Error class:

class InvalidOperationError extends Error {
	constructor(message, attemptedAction) {
    	super(message);
        this.attemptedAction = attemptedAction;
    }
}

You get access to all the methods and properties of the parent class for free - just by calling the super method. The runtime engine will look up the prototype chain for a matching method.

const myError = InvalidOperationError("You shouldn't have called this method like this, tsk.", "BAD_CALL");

console.log(myError.message)
// "You shouldn't have called this method like this, tsk."

console.log(myError.attemptedAction)
// "BAD_CALL"

Super cool! You can also make use of inheritance to do things like selectively handle exceptions, where previously pattern matching on properties was required.

fetch('/something-interesting')
	.then(response => response.json())
    .then(json => {
    	throw InvalidOperationError("Tsk tsk.")
    })
    .catch(err => {
    	if (err instanceof InvalidOperationError) {
        	// Swallow the error
            return;
        } 
        throw err;
    });

How Babel transpiles down ES6 classes

This is where we get to the interesting bit when it comes to transpiling your code. The way Babel 6 does transpilation of the class syntax breaks the prototype chain in a way such that builtins like Error cannot be overridden correctly; instanceof will evaluate err to be a simple Error object and the if block will return false regardless of what the err actually is. Yuck.

There are workarounds like the Babel plugin transform-builtin-extend, but they are imperfect and have implications for cross-browser support 😢

As the link above suggests, this is due to a limitation of the ES5 specification; there isn’t much you can do beyond installing the plugin and hope it works for your use case. It’s very non-obvious to new developers that this is a thing, and if you don’t do sufficient cross-browser testing or make use of the module hack to only serve transpiled code to legacy browsers, you may not even notice.

Especially since it’ll often be used in the examples such as mine above, and sometimes developers forget to test the unhappy paths as well as the happy paths, your exception handling code may be completely screwed.

The importance of educating yourself of the limitations of the abstractions you use when building software is clear here - make sure you take advantage of opportunities to learn and explore where you can.