3 Ways to Write More Defensive JS and Not Look So... Junior

Jan 14, 2023

 

The mark of the junior javascript developer: happy path coding.

Happy Pathing happens when you write code that doesn't consider anything outside the most basic cases. You expect your users and developers will always supply the correct arguments to your functions and APIs will return data in a format you expect.

Bless your heart. 

But No. Things will blow up taking this approach.

- Users will upload a .doc when they were clearly warned to send a .csv.

- Developers will refactor a function and miss an argument.

- APIs will blow up or change their JSON format unexpectedly.

 It's up to you to write more defensive code to protect against these realities. Here are 3 techniques I've learned over the years to write safer JS code:

1. Use Default Params

If you have a function which depends on an argument being present and being of a certain type then you should consider using default parameters in your function’s signature.

const addNameToNameArray = (name, nameArray) => {
  if(name.length){
   nameArray.push(name)
  }
  return nameArray
}

Simple enough right?

We take a name and a name array and then simply add the name to the array if one is present. We are making some assumptions here however: the

1. name will be of a type that has a length property

2. our nameArray will be, well, an array.

If either one of these conditions is not met, we will fail spectacularly.

Here's a slight refactor:

const addNameToNameArray = (name = '', nameArray = []) => {
  if(name.length){
   nameArray.push(name)
  }
  return nameArray
}

Ahh, much better. Now we can sleep at night, knowing this useless function won’t break production once it’s deployed if it’s called with a missing argument.

2. Use An Object as an Argument

Your team had this nice little function that accepted 3 arguments and did some straightforward logic with it to fetch some data from a third party service. But that was the past and now this little function is nearly unrecognizable and has a whopping 6 arguments getting passed to it.

Now, usually an argument with more than 3 arguments would be a code smell, but there are times you can’t avoid it.

const createUser = (name, date, profileId, dataId, dataType, apiKey) => {
  const user = {
   name,
   profileId
  };  
  const formattedDate = moment(date).unix();
  thirdPartyAPI.fetch(user, formattedDate, dataId, dataType, apiKey)
}
createUser('Bob', '04/20/20', 1, 123, 1234, 'story', 'abc123')

The danger with functions like these are that the order of the arguments matters and if any of them are incorrect, we risk blowing up our API call.

More arguments, means more surface area to make mistakes. It’s nearly inevitable that some developer will call this function and the profileId will be in the place of the dataId . Now this won’t blow up your program as they are both likely numbers or strings, but their incorrect order will give you the wrong data from the API or worse, just not work.

Let's refactor this to use an object as an argument:


const createUser = ({ 
 name,
 date,
 profileId,
 dataId,
 dataType,
 apiKey
}) => {
  const user = { name, profileId };
  const formattedDate = moment(date).unix();   
 
  thirdPartyAPI.fetch(user, formattedDate, dataId, dataType, apiKey)
}
 
  createUser({ name: 'Bob', date: '04/20/20', profileId: 1, dataId: 123, dataType: 'story' apiKey: '123abc' })

Boom. Using an object, we actually place less cognitive load on our fellow developers and make sure that our arguments are explicitly set. For functions with more than 4 arguments, I feel like using an object is a must.

3. Optional Chaining

We deal a lot with objects in JavaScript. Many people famously, and wrongly, say that everything in JavaScript is an object. Well, that’s not quite true, but we do end up working with them and their properties often and digging out the useful info can be a pain and a bit dangerous if you’re not careful.

const hat = user.outfit.hat;

Harmless right? Well, if our user object doesn’t have an outfit property or that property is undefined then we will have an issue on our hands.

TypeError: Cannot read property 'hat' of undefined

Now, the first step in making sure we don’t get this error is to check at each level whether the next property exists, and we get stuck writing unsexy code like this

const hat = user && user.outfit && user.outfit.hat || 'hat';

We feel a little sad writing this and a whole hell of a lot less sexy. Thankfully JS developers can now take advantage of what Ruby and Coffeescript developers have had for years: optional chaining

const hat = user?.outfit?.hat || 'hat'

Mmmmm, satisfying. Now if our traversal fails at any point, our operation will short-circuit and return undefined. The code is both safer and more readable.

The Best Offense is Good Defense

To me, a lot of writing defensive JavaScript is simply putting up logical safeguards to protect your code from input you might not expect, but will accept. Hopefully some of these suggestions will prevent you from being woken up at 3am on a Saturday morning to fix a bug in production! 

 

Grab my Ultimate JS Developer Kit

Learn unit testing, how to fix your LinkedIn to stand out, DSA and a hell of a lot more.

I hate SPAM. I will never sell your information, for any reason.