TL;DR:

babel-plugin-fail-explicit-demo

The tale of Brendan Eich's notorious 10 days of writing what he was told would be Java's "sidekick" language in the browser, "JavaScript", has taken off like no one would have ever expected. But Brendan could only do so much in those 10 days and one of the things he didn't have time to do was add exceptions and error handling to the language.

JS was designed so that it would "fail silently". These issues still persist in the language.

Here's some examples of JS silent failure semantics:

const foo = []
foo[1000]      // No exception thrown
foo.bar        // No exception thrown

[] + []        // No exception thrown
{} + []        // No exception thrown
// etc..

Fixing JavaScript

Why can't you just fix the problems in JavaScript?

TC39, the committee that decides the language specification for JS, follows a "Don't break the web" philosophy. All changes to the language must be semantically backwards compatible with all previous versions of the language.

There are also a number of attempts to fix the silent failure semantics of JS. Some time ago, Google proposed a "Strong Mode" for JS, which would eliminate all silent failure semantics from JS in a backwards compatible way. Unfortunately, the proposal did not pass and we're still left with the same broken semantics.

A New Hope: Transpilers

A couple years go by and writing vanilla JS has become a dreaded pastime. Transpilers, namely Babel, have taken the JS community by storm. Transpilers allow transformation of JS source code to compiled JS source code. What if we could write unsafe JS code and transform it to a stricter subset?

That's exactly what I've been experimenting with for a few weeks and the results have been quite promising:

What it does

Here's a simple example that shows failure of binary operators (-, +, *, etc) with unexpected types:

Here's a more real world example that shows:

  • Failure on access of non-existent property
  • Index out of bounds errors

Here's an informal spec of what the plugin (each will throw a TypeError on failure):

  • Prevents out of bound array access
  • Prevents access of non-existent property
  • Allows usage of + (and other forms: +=, etc) only between strings and numbers. Other binary operators will fail on operation that contains at least one expression that is not a number
  • Only comparisons between strings and numbers is
    allowed. Comparisons are only allowed between the same type. ex: '12' > 12 throws while 12 > 12 does not

How it works

The transformations that are applied are pretty simple: all binary expressions are wrapped in a function that checks the types of each expression and throws when the types are invalid. Here's an example of the comparison transformation:

{} + []

// transforms to:

safeComparison({}, "+", [])

Other transformations are similar. Here's the property access transformation:

const some = {}
some.foo.bar.baz

// transforms to:

safePropertyAccess(some, ['foo', 'bar', 'baz'])

Trying it out

Using the demo
git clone https://github.com/amilajack/babel-plugin-fail-explicit-demo.git
cd babel-plugin-fail-explicit-demo
npm i -g babel-cli
babel-node index.js
Manual install:
npm i babel-plugin-fail-explicit
npm i transform-es2015-modules-commonjs
npm i -g babel-cli

Create a .babelrc file:

{
  "plugins": [
    "fail-explicit",
    "transform-es2015-modules-commonjs"
  ]
}

Write some JS:

// index.js
const some = []
some += 12

Run babel-node index.js ? ? ?

Challenges

Transforming code and wrapping expressions in functions comes at the cost of debugging the code. The stacktraces of the transformed code is far more convoluted when compared to those of source code. Here's what the stack trace of the transformed code looks like for the following source:

const foo = []
foo[1111]
/safe-access-check/lib/index.js
          throw new TypeError(`"${separators.join('')}" is out of bounds`);
          ^

TypeError: "Array[1111]" is out of bounds
    at protoChain.forEach.each
    at Array.forEach (native)
    at safePropertyAccess
    at Object.<anonymous>
    at Module._compile
    at loader
    at Object.require.extensions.(anonymous function)
    at Module.load
    at tryModuleLoad
    at Function.Module._load

Ideally, safePropertyAccess() and other added checks could be removed from the stack trace, giving the impression that the checks were made natively by the language. I have noticed that flow-runtime somehow does this. The way it manipulates the stack trace is definitely worth investigating.

What's Next

I'm currently looking for feedback from users and language designers. I'd really appreciate test case contributions or bug reports.