Let’s take a look at a function that took a score between 1 and 100 and returned a rating out of 5.

function rating(score) {
  return Math.ceil(score / 20);
}

At first glance, this function looks simple, it has made a lot of assumption about its input argument. The obvious assumption is that the score is between 1 and 100. The not so obvious assumption is that it is a number. As Javascript is an un-typed language, any client calling this function can pass in the score as "21" or even "foo".

What should the return value of this function be if the input score is a string? Should it return NaN? throw an exception? or return -1?

It may be that in the context where this function is being used, it will never get into the erroneous state. For example, rating(toPercentage(score, 1, 10)). Let’s assume that toPercentage is returning a percentage given a score between 1 and 10. So, in this context, we can see that the function rating is actually used to convert a score from 1 to 10 to 1 to 5. Perhaps, toPercentage handles the case when the initial score variable is not a number between 1, to 10 (by capping the value), or the case when the score is a string returning 0 etc. We don’t know without looking in the source code of the function. The toPercentage function could’ve been provided by an npm package maintained by a different team. Who knows, they might start returning you a percentage as a string in later versions. This has happened to me in a project where the score was returned by an API call. And went from being a number to being a string.

How would you know if the score variable starts behaving differently to when you wrote the rating method? How can we catch this in production and make fixing it easier.

How about writting these assumptions down?

function rating(score) {
  console.assert(
    score !== null || score !== undefined,
    "expected score to be not null or undefined"
  );
  console.assert(Number.isFinite(score), "expected score to be a valid number");
  console.assert(
    score >= 1 && score <= 100,
    "expected score to be between 1 and 100"
  );
  return Math.ceil(score / 20);
}

This is starting to look a lot like defensive programming. A method should always validate its input. Refactoring by Martin Fowler has documented this as Introduce Assertions. This seems a bit verbose and the lines of code have gone from 1 to 4.

function rating(score) {
  assert.isBetween(1, 100, "score", score);
  return Math.ceil(score / 20);
}

In this variation, assert.isBetween function can handle the score being undefined or null. Also ensuring the type being a number and finally in the acceptable range.

function isBetween(lower, upper, name, value) {
  console.assert(
    score !== null || score !== undefined,
    `expected ${name} to be not null or undefined`
  );
  console.assert(
    Number.isFinite(value),
    `expected ${name} to be a valid number, but was ${value}.`
  );
  console.assert(
    value >= lower && value <= upper,
    `expected ${name} to be between ${lower} and ${upper}.`
  );
  return true;
}

Conclusion

Even though defensive coding can get verbose, the benefits of doing so can help diagnose errors. I have seen this kind of coding in C#, I have not run into it in Javascript codebases. Given the untyped nature of the language, it seems logical to start writing code this way. The isBetween assertion function is verbose to make it easier to understand. Using a library such as ramdajs, can help compose the isBetween function, shortening it.