Defensive coding with guard assertions in Javascript
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.