What I Learned at Work this Week: Preventing Errors

Photo by Vie Studio from Pexels

I learn something new every day at my job, though not always something worthy of an entire blog post. This week my biggest insight wasn’t code specific: I realized that not every obstacle was based on a lack of technical knowledge when it turned out that my code wasn’t working because I had overlooked a potential user input (a site that did not require 10 digits in a phone number field). But since there’s not much of a story there, I’ll devote this blog to something I learned shortly after being hired that affects more than 50% of the code I wrote: preventing errors.

Writing and implementing software provides massive opportunity for error. Sometimes code won’t work the way we expect it to, like when I was looking for those phone numbers, and we could collect or display errant data. Sometimes our integrations will fail and our software won’t do its job. Sometimes our environment will change and we’ll have to update a configuration. Errors are so common that it’s standard to see plenty of red in the console on many well-built and well maintained websites.

The good news here is that nytimes.com is working just fine. I’m able to browse and use features on the site like clicking links and searching for articles, so the user experience isn’t impacted by these errors (and actually they went away when I checked back the next day). But these errors are being generated by a process that’s supposed to finish, even if it’s not something currently impacting the user experience. At worst, they’re part of a function that’s supposed to return a value which isn’t going to be returned. But if the function’s failure to finish isn’t an issue for us, it shouldn’t be throwing a big red error — we should be handling that from inside of our code.

Since most of the work I do is in JavaScript, I’ll use a common JS error as a template: the Uncaught TypeError. We saw this present itself in my screenshot from nytimes.com. It often occurs when we are trying to access data inside of a nested object but reach an undefined value. Going back to our previous example, I can access a dataLayer from the window object on nytimes.com:

Loyal readers might remember that a dataLayer is an array of objects containing information about events that have occurred during our browsing of a site. Let’s say I wanted to look at the first object:

This object has an abtest key that will lead to another object called batch. If I wanted to return the array associated with batch, I could use the command:

window.dataLayer[0].abtest.batch

Alarm bells might already be going off for some readers — what if the order of elements in the dataLayer array changes? Will our results always be the same? Let’s see what happens if I run this command on a different element:

We see an Uncaught TypeError telling us that we can’t read the property ‘batch’ of undefined. Looking at our command, batch is being applied to window.dataLayer[3].abtest, which is apparently undefined. If we trace our command back one more level, we see the contents of window.dataLayer[3]:

This object does not contain a key called abtest, so making that query returns undefined. Attempting to make a query on that undefined object gives us the Uncaught TypeError message. This is a problem for us because we want to be able to retrieve this data programmatically, but if the order of elements changes and we get an error, it’s going to break our code. How do we deal with this?

Try-Catch syntax is a neat, readable way to handle errors like ours. It looks like this:

try {
window.dataLayer[3].abtest.batch
} catch (e) {
// do nothing
}

If and when we hit the Uncaught TypeError, it’ll trigger our catch logic. The argument in the catch parentheses is an error object that can be useful in communicating the error, though when I see it at work it’s generally unused (hence the curly brackets containing just a comment). Ultimately the benefit here is we can query a nested object without worrying that it will produce a console error or break our code.

Try-Catch is a tried and true method of error handling, so it might be surprising to learn that it’s not the most common technique I see applied in the workplace. Instead, it’s much more common to prevent errors through manual logical requirements that involve the && operator.

const dLayer = window.dataLayer
const testObj = dLayer && dLayer[0]
const abObj = testObj && testObj.abtest
return abObj && abObj.batch

This is known as short-circuiting and it works by requiring the truthiness of a variable before running additional logic. If window.datalayer doesn’t exist, we won’t try to query for an index. If that index doesn’t exist, we won’t try to find the abtest attribute. If and when abtest returns undefined, we won’t even attempt .batch, which means we’ll never get our Uncaught TypeError.

If this is confusing, consider the logic of an AND statement:

true && true => truetrue && false => falsefalse && true => falsefalse && false => false

If the first half of our statement is true, we have to evaluate the second half to know whether it will be true or false. But if the first half is false, the second half doesn’t have to be evaluated at all. And in fact, it won’t be! If window.dataLayer returns a falsey value, window.dataLayer[0] won’t run.

We can use similar logic to assure that whatever we return is a specific data type in case we plan to use it for future operations. Instead of &&, we’d use ||:

Return myEvaluatedVariable || 'missing value'

If myEvaluatedVariable is undefined or otherwise falsey, this will return the string ‘missing value’, which might be useful if we’re going to display this value somewhere else. Let’s quickly look at the logic of an OR statement:

true || true => truetrue || false => truefalse || true => truefalse || false => false

If the first half of our statement is truthy, the return of our statement will be true regardless of the second half. But if the first half is falsey, the second half will determine the return. Likewise, in our logic above, we’ll return the evaluated variable unless it’s falsey, in which case we’ll return the backup string.

The latest in JavaScript offers us some additional guardrails for even more control over our queries. For instance, || checks against falsey values, but we can be more specific by using the nullish coalescing operator, or ??. Nullish values include null and undefined, but not empty strings, the number 0, or NaN.

But we also saw an even bigger change this year: optional chaining. This is essentially what we’ve been trying to do with our try-catch and our short-circuiting: we want our code to run if we’ve made a valid query and harmlessly return undefined if not. What does it look like?

window?.dataLayer?.[0]?.abtest?.batch

We just add ?. to our object queries, function calls, and even array index references, and we can see it working even when we know the objects aren’t there. It’s a great new alternative that will hopefully become more widely adopted in the future.

This blog actually has almost no sources to cite because I’ve been able to put together a fairly good understanding of this subject! I came across a great list of new JS features from ES2020 that I encourage everyone to check out. Read it here.

Solutions Engineer