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 | function add(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 | function plot(xvalues) { |
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.