Errors get a bad rep in the newborn #devjokes lingo, which is just a few years old at this point. Btw did you notice when words like programming, algorithm, software, and AI became part of the everyday expression π€π€, extra points to the person who can tell the catalyst of this reaction ππ?
Anyways, my point is errors are not bad at all.
I would even go as far as saying errors are great, they are like that friend who tells you his honest opinion and notifies you when you do something wrong.
Enough babbling, let's look at an example.
const TASKS = ["Task 1", "Task 2", "Task 3", "Task 4"];
function getNextTask(currentTask) {
let currentIdx = TASKS.indexOf(currentTask);
return TASKS[currentIdx + 1];
}
we created a function that returns the next task from the TASKS
Array, using currentTask
as its input. We can try the function and it works, until it doesn't.
If we look closely, we will observe that we left a segmentation fault
error in our code("segmentation fault" is just an OG way of the compiler telling us, it can't find the thing we are looking for as the index is incorrect).
And since we have found out the problem, it's an easy fix.
const TASKS = ["Task 1", "Task 2", "Task 3", "Task 4"];
function getNextTask(currentTask) {
let lastIdx = TASKS.length - 1;
let currentIdx = TASKS.indexOf(currentTask);
if(currentIdx >= lastIdx) {
return null;
}
return TASKS[currentIdx + 1];
}
Looking so much better already :)
Introducing Uncertainty π
Happy with the implementation we deploy it to production. Few days in, you suddenly observe that the function seems to have some error and the code is failing in production.
Our initial investigation tells us the function keeps returning the first element of the array. Can you guess what might be causing the issue ??
Congratulations to everyone, you all win the explanation, ready here goes π₯ π₯ π₯
The curious case of the missing item
Our investigation has finished, the errors seem to be caused when currentIdx
is -1
, which happens when the item is not found in the array.
Now that we know the issue, an obvious solution seems like handling this cause.
const TASKS = ["Task 1", "Task 2", "Task 3", "Task 4"];
function getNextTask(currentTask) {
let lastIdx = TASKS.length - 1;
let currentIdx = TASKS.indexOf(currentTask);
if(currentIdx === -1) return null;
if(currentIdx >= lastIdx) return null;
return TASKS[currentIdx + 1];
}
We fixed the issue and our function seems to work fine now.
The Problem
Considering the above function as a reference we fixed the issue by adding a defense condition which work. But the function getNextTask
was designed to return the next task which we accidentally changed to validateTaskAndReturnNextTask
, this is an issue but not a deal-breaker. The bigger problem here is, we didn't fix the problem, we just patched the inception point of the error, which can be lead to more unexpected issues.
Error Friendly Functions
We need to rethink how we write functions, make them more robust so that they make our software better.
const TASKS = ["Task 1", "Task 2", "Task 3", "Task 4"];
function getNextTask(currentTask) {
let lastIdx = TASKS.length - 1;
let currentIdx = TASKS.indexOf(currentTask);
if(currentIdx === -1) throw new Error(`Invalid task ${currentTask}`);
if(currentIdx >= lastIdx) return null;
return TASKS[currentIdx + 1];
}
When you look at the code above not a lot has changed, we updated the defense condition from a return to an error.
You might say I just increased your work and now you need to write try-catch
everywhere, to which I will say "No good sir you don't".
Don't handle the error
Yes, I am serious "don't handle errors". Yes I know, that would mean your application will crash, but that's exactly what we want.
Consider our getNextTask
function. In an ideal world, we will never get the case where currentTask
is not found in the TASKS
array. And since our function works on that assumption, it should shout on us when its conditions are not met and hence we throw an error.
With this idea of letting our applications crash, we made an unexpected superhero friend The Stack Trace.
Stack Traces are great, they give you an exact history of what happened and how it happened to maintain both state and order of execution of operations, which is extremely helpful in finding the actual error, which in our case would be the statement that makes the currentTask
an invalid entry in TASKS
array so that we can solve the actual problem and not just patch the problem inception point ππππ.
But leaving unhandled exceptions in our code is a terrible idea, especially when deploying to production. So, to deal with that we create a helper.
function reportError(message) {
if(process.env === "PRODUCTION") {
// Call to your logging service or bug tracker
// MyBugService.error(message)
} else {
throw new Error(message);
}
}
const TASKS = ["Task 1", "Task 2", "Task 3", "Task 4"];
function getNextTask(currentTask) {
let lastIdx = TASKS.length - 1;
let currentIdx = TASKS.indexOf(currentTask);
if(currentIdx === -1) reportError(`Invalid task ${currentTask}`);
if(currentIdx >= lastIdx) return null;
return TASKS[currentIdx + 1];
}
And with that, you just created a better critic for your code which can help you catch hidden bugs in the development phase and will help your make more robust software from the get-go.
Top comments (2)
Interesting post! "Yes I know, that would mean your application will crash, but that's exactly what we want." is something I have mentioned in the past a couple times (though I definitely don't always agree with it), where mis-configuration by person X had to be "resolved" by developer Y. Whereas ideally you would have the application crash, because something was configured in a way where certain functionality that was supposed to work didn't work.
The Action Pattern?
ponyfoo.com/articles/action-patter...