June 5, 2022

Are you writing Faulty Fallbacks?

Are you writing Faulty Fallbacks?

Most developers will write fallbacks simply because that is the standard way of doing things.

A faulty fallback is a default parameter that either:
1) Has no actual purpose
2) Obscures real problems

There are some standard mental models in programming that are so embedded in the culture that no-one ever thinks to ask, why am I actually doing this and is it saving us time or effort?

Faulty fallbacks are at best useless and at worst actively malicious.

A Simple Example

const fastify = require('fastify')({
  logger: true
})

fastify.listen(process.env.PORT || 3000, function (err, address) {
  if (err) {
    fastify.log.error(err)
    process.exit(1)
  }
  fastify.log.info(`server listening on ${address}`)
})

This is standard boilerplate that you might see in a Node service. Unfortunately this is standard boilerplate that I often see in node micro services.

For a single service developer, this is a totally reasonable thing to do. The problem is, most of us are not single service developers. We work in teams, with multiple developers working on multiple services at the same time. I currently have about 15 micro services running on my development machine. Every single micro service has a default port of 3000.

Who does this help?

By setting this default value, what we have actually done is put ourselves in a situation where each and every developer is forced to set a different value than the fallback, to prevent port clashes. For a developer, this fallback is absolutely useless.

You could argue that it makes deployment easier - because in containers you are likely to use the same internal port for every service. However, your deployments should be setting the values anyway, even if you are keeping them at the default, in order to ensure reproducibility.

In most cases your default parameters should be written to make the lives of developers easier, provided it doesn't make the life your other teams harder. It makes much more sense therefore, to give each service a unique default port number and have your deployment set the value to one it wants to use.

When things get malicious

The worst type of faulty fallback is the kind where the parameter is set to something that works, but is wrong.

var nodemailer = require('nodemailer');
var transporter = nodemailer.createTransport({
    host: 'smtp.gmail.com',
    port: 465,
    secure: true, // use SSL
    auth: {
        user: process.env.EMAIL_USER || developer@asyncresolve.com,
        pass: process.env.EMAIL_PASS || 'super_secret_developer_password'
    }
});

In this example we have prioritized the workflow of the developer by configuring our email sender with our development credentials as the defaults. This is great for developers. They don't have to set any environment variables and they can just get on with coding or debugging. The problem occurs when someone either forgets or doesn't know that these values need to be set in production.

Your customers are not receiving any emails and your dev ops team doesn't even know that there is an environment variable that needs to be set. The system doesn't report any errors. In fact the system is happily reporting that all emails are being sent successfully. A problem like this could go undetected for days.

What to do about faulty fallbacks

  1. Don't write unnecessary fallbacks. Throw errors instead, and force everyone to set environment variables manually
  2. If you really must write default parameters, always ask yourself, who does this benefit, what issues will this create and how will we know those issues have happened

For developers create a development.env file with reasonable defaults and add this to your code repository. Automatically source this into the running terminal or add it to your debug set up (I add it to my VSCode launch.json).
This way, you can avoid setting unnecessary defaults altogether and you have the added benefit of maintaining a single file that necessarily documents every possible environment variable.