[Developer says] Making assumptions explicit in Javascript

Javascript is a language that is, by design, difficult to debug. It is dynamically typed, it is based around asynchronous callbacks, almost all data types are mutable, almost no functions of the standard library are pure, and the list goes on. And these are not just flaws in the design; they are what makes the language what it is. It is therefore important to be equipped with the correct tools when setting out to correct your code. I am not talking about fancy debuggers and static code analyzers, just two very simple libraries. So simple in fact they do not even qualify as separate libraries. They are just a couple of hundreds of locs that you can copy right into your project and modify to your needs. My sample implementation for each is provided.

Typechecker

For most programmers it goes without saying that you should always keep your types in check. In javascript however that does not come automatically, you need to do it by hand. Of course humans are very bad at keeping track of anything so it's necessary to find some way of tracking (or at least asserting) the types automatically, mostly so that when your code fails, it fails as soon as possible after something unexpected happens.

When designing the mechanism for type assertions we also want to promote two programming philosophies:

  • Favoring function composition to global state
  • And keeping functions as concise as possible, always preferring short code to a short stack.

For these two reasons we will make it the easiest to check function argument types, rather than generalizing. Also note that we assume that functions will much more often accept callbacks as arguments instead of returning. For these reasons we provide exactly two functions that will check types at runtime:

typecheck(arguments, ['number', 'function']);

Will throw an error if the arguments list is not a two element array containing a number and a function. The other method for this is typechecked:

typechecked(fn, ['number', 'function']);

This will return a wrapped function fn with exactly the above check.

This method, while making runtime checks rather than giving compile time guarantees, not only helps immensely in quickly finding the place of the error but also serves as code documentation. Let's look at an example. Say we have this code:

function f3 (n) {
    console.log("The number is:", n);
}
function f2 (c) {
    setTimeout(f3.bind(c + 7), 10);
}
function f1 (a, b) {
    return f2 (b+1);
}

f1("one arg", "1");

This will give:

$ node test.js
The number is: undefined

Not what we expected… normally we would need to litter the code with console.log or even worse with breakpoints. Instead let's apply our library to it:

var typecheck = require('./typecheck.js').typecheck;

function f3 (n) {
    typecheck(arguments, ['number']);
    console.log("The number is:", n);
}
function f2 (c) {
    typecheck(arguments, ['number']);
    setTimeout(f3.bind(c + 7), 10);
}
function f1 (a, b) {
    typecheck(arguments, ['string', 'number']);
    return f2 (b+1);
}

f1("one arg", "1");

We will happily get an error telling us exactly what went wrong and where.

/Current/directory/typecheck.js:144
  throw Error(m.error);
  ^

Error: Argument #1: Expected number got 1
    at Error (native)
    at typecheck (/Current/directory/typecheck.js:144:9)
    at f1 (/Current/directory/test.js:12:3)
    at Object. (/Current/directory/test.js:16:1)
    at Module._compile (module.js:541:32)
    at Object.Module._extensions..js (module.js:550:10)
    at Module.load (module.js:458:32)
    at tryModuleLoad (module.js:417:12)
    at Function.Module._load (module.js:409:3)
    at Function.Module.runMain (module.js:575:10)

The 4th line of the stack trace tells us exactly where the error took place!

Here are a few more sample checks you can perform with sample implementation provided.

// Match: f('a',1,2,3,4,1)
typecheck(arguments, ['string', 'varany', 'number']);
// Match: f('a', [1,2,3], new A())
typecheck(arguments, ['string', 'any', A]);
// Match: f('a', {a:1, b:{c:2,d:3}})
typecheck(arguments, ['string', {b:{c:'number'}}]);

Checkpoints

Asserting that the types of your function's arguments are what you assume they are can save a long time tracking down bugs but that is not the only pain when debugging javascript. The second most common source of errors, in my experience, is asynchronous closures being called in an unexpected order. Like most problems in life there is no perfect solution but we can do better than simply hoping things happen in the right order.

Let's take a simple example:

setTimeout(function a () {
    // We assume this is reached first
    setTimeout(function b () {
        // Then this
    });
});

setTimeout(function c () {
    // Finally this
}, 100)

But what if the call queue is really, really busy and c is already in the queue when a is called? c will be called before b. This is a very contrived race condition and you will probably not catch it even in your tests without randomizing the scheduler. However, you can try to catch close calls by explicitly declaring your assumptions about the order in which code is executed, either in the code itself, or from the tests.

To do this we created checkpoints , which is also not extensive or complex enough to qualify as a separate package. The premise is simple: Each checkpoint is denoted by an integer and a stateful object (the CheckpointManager) throws when the checkpoints are called out of order. For example:

var assert = require('assert'),
    CheckpointManager = require('checkpoints');

describe("serial.test", function () {
    var cm = null;
    beforeEach(function () {
        // The state of the test is managed by the checkpoint manager.
        cm = new CheckpointManager();
    });

    it("events", function (done) {
        // Doesn't matter which one comes first
        setTimeout(function () {
            cm.setCheckpoint(0);
        });
        setTimeout(function () {
            cm.setCheckpoint(0);
        });

        // This one should be after
        setTimeout(function () {
            cm.setCheckpoint(1);
        }, 10);

        setTimeout(function () {
            // The latest checkpoint encountered should be 1
            // checkpoint 0 should have been encountered twice, it
            // goes without saying that checkpoint 1 should have been
            // encoundetered once. Closing the manager is optional but
            // without it only correct sequencing of the checkpoints
            // is enforced.
            cm.close(1, {0:2});
            done();
        }, 30);
    });
});

Conclusion

With javascript it is really easy to keep your assumptions about your code implicit, thereby hurting readability and allowing space for bugs. The point of this article is not so much to show off "2 cool ways of debugging javscript" code but to make a point about how a programmer should strive to make it easy and mandatory for themselves to express and verify even the most basic assumptions about their code and the problem they are solving. This is especially the ones that do not show up on the whiteboard.