Defensive coding examples

Examples of using lodash, angular, check-types, ramda assertions

Hunting for bugs in the production minified JavaScript code is always fun. Having an intelligent error message helps a lot, especially if the message warns about the first thing that goes wrong. That is why I practice defensive and even paranoid coding style. Each public facing function checks its arguments and environment before doing any work.

1
2
3
4
5
6
7
8
9
function add(a, b) {
if (a is not a valid number) {
throw new Error('invalid first number to add ' + a);
}
if (b is not a valid number) {
throw new Error('invalid second number to add ' + b);
}
return a + b;
}

There are three sources of errors to guard against

  • Logical errors in the code itself.
  • Errors in the input data, especially user supplied input.
  • Errors in the environment

You can fight logical errors with unit tests that cover all code paths using code coverage tools.

Input data errors

You can safely assume that at some point your externally visible functions will get junk inputs. User supplied data or information coming from the server should never be trusted. Always do a sanity check against the data before doing any calculations. An immediate exception will make hunting for the source much easier. Decouple raising the error from trying to handle it. For example do not do this:

function add(a, b) {
  if (a is not a valid number) {
    a = 0; // BAD!
  }
  ...
}

Instead let the code calling add handle any errors

function calc() {
  var a = form().a;
  var b = form().b;
  try {
    return add(a, b);
  } catch (err) {
    showErrorMessage(err);
  }
}

The sanity checks should be O(1) if possible, otherwise they will slow down the system. For example, if the input is an array to be processed, do not check the entire array. Maybe you could get away with just testing the array length to be positive in order to guard against the junk data.

Environment errors

You might assume something about the environment, but your code is used in a different setting. For example, you might assume your D3 code is running in a browser with full access to window object and parent element this. What if it is being unit tested under Nodejs? What if the parent element is invisible and has its width set to zero?

I would not check the environment by default. I would add a sanity check the first time I see a bug related to the environment assumptions. The only exception is checking for function existance that might be used in very special cases, for example error handling support. I want to check if they exist right away to make sure when an error happens, the handler exists.

(function myCode() {
  // check if errorHandler exists right away
  if (typeof errorHandler === 'undefined') {
    throw new Error('undefined error handler');
  }

  ... deep down somewhere
  if (condition) {
    // rare error, but the handler is ready to do something
    errorHandler('Something went wrong');
  }
}());

Helper functions

In a static language like C or Java, the compiler would perform some of the sanity checks for us, for example not allowing to pass string where an Array is expected. Unfortunately, the truly useful sanity checks go beyond simple types. I use the following 3 collections of test functions in my JavaScript code

Angular

Angular has the following test functions:

angular.isArray, angular.isDate, angular.isDefined
angular.isElement, angular.isFunction, angular.isNumber, angular.isObject
angular.isString, angular.isUndefined

Lodash / Underscore

Lodash has the following test functions:

_.isArguments, _.isArray, _.isBoolean, _.isDate,
_.isElement, _.isEmpty, _.isEqual, _.isFinite,
_.isFunction, _.isNaN, _.isNull, _.isNumber, _.isObject,
_.isPlainObject, _.isRegExp, _.isString, _.isUndefined

My favorite is _.isFinite that guards against undefined and NaN values.

check-types

For more advanced checking, I sometimes use check-types library. It even allows throwing an exception in a single call

var verify = check.verify;
function plot(xvalues) {
  verify.array(xvalues, 'need an array of numbers to plot');
  verify.positiveNumber(xvalues.length, 'empty array to plot');
  ...
}

check-types has verify function to check an object's structure using duck typing and batch operations, see examples

We have released additional helpers that build upon check-types, see check-more-types.

Ramda

Ramda has multiple logical predicates, including low-level type checking and combinators

R.is(Object, {}); //=> true
R.is(Number, 1); //=> true
// a few other types can be checked
R.isArrayLike, R.isEmpty, R.isSet // isSet checks if all elements are unique

Logical combinators included into Ramda:

R.and, R.or, R.not, R.ifElse, R.allPredicates, R.anyPredicates
R.cond // builds "switch" statement from conditions
var fn = R.cond(
   [R.eq(0),      R.always('water freezes at 0°C')],
   [R.eq(100),    R.always('water boils at 100°C')],
   [R.alwaysTrue, function(temp) { return 'nothing special happens at ' + temp + '°C'; }]
);
fn(0); //=> 'water freezes at 0°C'
fn(50); //=> 'nothing special happens at 50°C'
fn(100); //=> 'water boils at 100°C'

Example

Here is a small example from D3 charts. We are computing the chart's scale and need to guard agaist both the input data errors and environment assumptions

function plot(xvalues) {
  var xrange = d3.extent(xvalues)
  var scale = d3.scale.linear()
    .domain(xrange)
    .range([0, this.element.width()]);

d3.extent can silently return garbage data for invalid inputs, for example

d3.extent([]);    // [undefined, undefined]
d3.extent('foo'); // ['foo', 'foo']

Let's add a guard against the invalid input data. Second, let's check the returned xrange, otherwise we will probably get either empty chart, or weird NaN console errors.

1
2
3
4
5
6
7
8
9
10
11
function plot(xvalues) {
if (!_.isArray(xvalues) || !xvalues.length) {
throw new Error('Invalid data to plot ' +
JSON.stringify(xvalues, null, 2));
}
var xrange = d3.extent(xvalues);
if (!_.isFinite(xrange[0]) || !_.isFinite(xrange[1])) {
throw new Error('Could not determine range from data ' +
JSON.stringify(xvalues, null, 2));
}
}

I would add the above sanity checks by default when I code the function the first time. If I see some weird behavior, then I would also check the data going into the chart's range.

function plot(xvalues) {
  ... previous checks
  if (this.element.width() < 1) {
    throw new Error('Invalid element width ' + this.element.width());
  }
  ... scale domain and range computation
    .range([0, this.element.width()]);

Do not use console.assert

Whenever there is a problem, throw an Error object, because console.assert only prints an error message, it does not actually stop the script execution in the browser (unlike in Node). Thus you will never know about the error, but only pollute the JavaScript console with messages.

I use Sentry error reporting to learn about all errors our users are experiencing in real time.

To guarantee a thrown error and avoid performance hit when passing multiple arguments, I wrote a library of lazy assertions lazy-ass.