On bad eggs

There comes a time in every application’s life when the optimism of early development is eclipsed by the harsh spectre of deployment into userland. In the best case, thorough tests have covered every conceivable branch of execution; in reality, pressure from deadlines and the project budget often limit the reach and rigor of the testing process. And when comprehensive testing becomes an inaccessible luxury, bad things will happen. The question is when, and where.

For just a moment, forget about the tests and look at the code. Can the time required to validate and approve it be reduced? While the answer will depend on the specific code in question, the general answer is a qualified “yes”. Take an example

Maybe the application includes a basket full of eggs. It starts with a certain collection of eggs (startingEggs) and will accept additional eggs to be added until it is full. It may be used within an application to track the number of eggs a user has purchased, but also may be exposed to eggs added from outside the application.

Here’s an Egg:

// An egg
var Egg = function (type) {
  this.type = type;
};

And here’s the basket:

// A basket
var EggBasket = function (startingEggs) {
  var eggs = startingEggs || [];

  this.add = function (egg) {
    if (!this.isFull()) {
      eggs.push(egg);    
    }
  };

  this.isFull = function () {
    return eggs.length == 12;
  };
};

There are some serious issues here.

  1. The basket expects startingEggs to be an array, but does not actually check type
  2. The basket excepts to receive eggs, but does not confirm what they are
  3. The basket does not check whether all of the startingEggs will fit in the basket

In layman’s terms, suffice it to say that a swagman injected into a basket that expected to receive some eggs would be living the good life:

var swagMan = {
  length: 0,
  push: function (stolenEgg) {
    this.tuckerBag = this.tuckerBag || [];
    this.tuckerBag.push(stolenEgg);
  }
};

var basket = new EggBasket(swagMan);

Guess what happens next?

for (var i = 0; i < 13; i++) {
  basket.add(new Egg('free-range'));
}

console.log(basket.isFull()); // false
console.log(swagMan.tuckerBag.length); // 13

That’s not good. Really not good.

Getting defensive

So what went wrong? The original EggBasket operated under a simple contract: if you put eggs in, I’ll continue storing them until I’m full. And so long as everyone followed the contact, it did exactly what it was supposed to do.

The trouble began the moment a bad egg got into the system. Rather than recognizing the swagMan for what he was, the basket welcomed him with open arms. A better implementation of EggBasket would be much more proactive in ensuring that the eggs it contains are, in fact, eggs. This simple validation won’t magically cover every contingency, but it will at least offer control over everything flowing in to the basket.

var EggBasket = function (startingEggs) {

  var eggs = [];

  this.add = function (egg) {
    // check that egg is an `Egg`
    if (egg instanceof Egg && !this.isFull()) {
      eggs.push(egg);
    }
  };

  this.isFull = function () {
    return eggs.length == 12;
  };

  // Add starting eggs, if provided:
  if (startingEggs instanceof Array) {
    var i = startingEggs.length;
    while (i--) {
      this.add(startingEggs[i]);
    };
  }
};

Much better: the basket will now accept valid eggs and quietly ignore any that don’t pass muster. There are additional attacks possible against specific facets of the class (can you spot them?) but their overt effects are much more limited than before. Pass a bad egg? The new EggBasket turns up its nose in disgusted refusal.

Taking exception

There is one more case to consider. If invalid eggs are being passed to the EggBasket from somewhere within the application, wouldn’t it be nice to know? Errors discarded silently become impossible to trace. Testing time goes up. Development time goes down. If an error is occurring internally, wouldn’t it make sense to drag it kicking and scream to the fore?

Inside the application, EggBasket might adopt a different error-handling strategy. In its final variation, exceptions are liberally raised not only to stop the program, but also to provide a descriptive error and backtrace when invalid conditions are found.

var EggBasket = function (startingEggs) {

  var eggs = [];

  this.add = function (egg) {
    if (!(egg instanceof Egg)) {
      throw('Bad egg!');
    }

    if (!this.isFull()) {
      eggs.push(egg);
    }
  };

  this.isFull = function () {
    return eggs.length == 12;
  };

  if (startingEggs instanceof Array) {
    var i = startingEggs.length;
    while (i--) {
      this.add(startingEggs[i]);
    };
  }
  else {
    throw('Bad starting eggs!');
  }
};

Once again, a tiny increase in code translates into a big step forward for debugging. Trouble in the basket will now be forwarded to the debugging console, where its source may be immediately addressed. No need for a convoluted web of error messages—now, the error bubbles right up to the top.

Let’s keep in touch

Reach out on Twitter or subscribe for (very) occasional updates.

Hey, I'm RJ: digital entomologist and intermittent micropoet, writing from the beautiful Rose City.