Aral Balkan

Mastodon icon RSS feed icon

Fail-fast on missing required arguments in JavaScript using default values that throw

Screen grab of road running with an ACME box exploding on his face

Missing arguments are not fun.

I’ve been working on Auto Encrypt to use in Site.js for the past few weeks and it’s at the point now where I’m going through the codebase to increase coverage and improve quality. As part of that process, a few moments ago, I had an idea about implementing error-checking on missing arguments that I thought I’d share with you.

The problem

JavaScript doesn’t automatically throw an error if the argument for a required parameter is missing. Which means that it is possible for things to fail not where the problem is (at the function call) but later on.

To avoid that, you have to implement some sort of error checking yourself. However, doing so is rather verbose and adds noise to your functions. And so it’s rarely done.

function foo (required, optional = 'default value') {
  if (required == undefined) {
    throw new Error(`Missing argument.`)
  }
  // It’s safe to use required parameter from here on.
}

In the example above1, you can see how things would quickly get out of hand if you had, say, four or five required arguments. You’d have to write (and read through) a lot of filler before getting to the crux of your method.

Ugly. And error prone.

In the listing, you also see an example of an optional argument that has a default value specified. What if there was a more elegant way of failing fast on missing required arguments using optional argument syntax? Spoiler: there is!

So check this out:

const required = () => { throw new Error('Missing argument.') }

function foo (required = required(), optional = 'default value') {
  // It’s safe to use required parameter from here on.
}

Well that’s nicer to read, isn’t it?

What we’re doing is converting the required argument into an optional argument but specifying its default value as a method call that throws.

Of course, you can pop that required() function into a module and re-use it in all your files:

// required.js
module.exports = function () {
  throw new Error('Missing argument.')
}
// index.js
const required = require('./required')

function foo (required = required(), optional = 'default') {
  // It’s safe to use required parameter from here on.
}

Pretty neat, huh?

It would be even better if we didn’t have to keep the poor developer struggling with your code (probably future you) in the dark about exactly which argument is missing:

// required.js
module.exports = function (argumentName = false) {
  throw new Error(`Missing argument${argumentName ? ` (${argumentName})` : ''}.`)
}
// index.js
const required = require('./required')

function foo (puppy = required('puppy')) {
  // It’s safe to use puppy from here on.
}

// Output: Error: Missing argument (puppy).
foo()

I haven’t actually implemented this in Auto Encrypt yet (or Site.js, or anywhere else) but I think I’m going to. I have already started using a mixin to provide better error handling using Symbols in Auto Encrypt so I will most likely add this to that.

Thoughts? Questions? Suggestions? Hit me up on the fediverse..

Update

I just chucked this up on npm (source code) so you can use it in your projects via:

npm i @small-tech/required
// index.js
const required = require('@small-tech/required')

function foo (puppy = required('puppy')) {
  // It’s safe to use puppy from here on.
}

// Output: Error: Missing argument (puppy).
foo()

See what I did there? If this becomes popular, I look forward to seeing “small tech required” in source code listings everywhere ;)

Update #2

Paul suggested on the fediverse that I use Error.captureStackTrace() to remove the required() function itself from the error’s stack trace, so I did. (Thanks, Paul!) With that change, the module looks like this:

// required.js
function required (argumentName = false) {
  const error = new Error(`Missing argument${argumentName ? ` (${argumentName})` : ''}.`)
  Error.captureStackTrace(error, required)
  throw error
}

module.exports = required

You can find the updated version in the npm module. And I went ahead and implemented 100% code coverage for the module because apparently I have no life :)

Like this? Fund us!

Small Technology Foundation is a tiny, independent not-for-profit.

We exist in part thanks to patronage by people like you. If you share our vision and want to support our work, please become a patron or donate to us today and help us continue to exist.


  1. In the examples, we’re using loose equality (==) to ensure that the argument is not undefined or null. If you wanted to allow null, you would use strict equality here (===). ↩︎