DEV Community

George Campbell
George Campbell

Posted on • Updated on

How to handle errors

Random code

There are many cases where an error case comes up in code, and us lazy developers like to just log the potential error to the console and move on.

function divide(a, b){
  //handle divide by zero case
  if(b === 0){
    return console.error("Cannot divide by 0");
  }
  return a / b;
}
Enter fullscreen mode Exit fullscreen mode

This might feel sufficient, but would you really write this for a production application?

The main problem with this approach is that it sweeps the problem under the carpet, to deal with later.

As your program grows, your console will start filling up with a messy set of clever little messages hinting at what's wrong.

The first improvement would be to take advantage of JavaScript's built in error support using Error objects, to give your console messages a lot more useful information.

function divide(a, b){
  //handle divide by zero case
  if(b === 0){
    throw new Error("Cannot divide by 0");
  }
  if(typeof a != 'number' || typeof b != 'number'){
    throw new TypeError('Can only divide two numbers, invalid type.');
  }
  return a / b;
}
Enter fullscreen mode Exit fullscreen mode

Now, when you try to divide by zero somewhere in your program, the console will give you a detailed set of information which will help you find source of the error, telling you where divide was called in a stack trace.

However, we still have an unhandled error somewhere in our application, so what do we do?

Use try { } catch (err) { }!

//Fake calculator app with pseudo code
const inputTerms = [10, 'divide', 'bracket', 2, 'minus', 'ceil', 1, 'point', 4, 'bracket']

function calculateResult(inputTerms){
  ...
  inputTerms.reduce((num, term) => {
    ...
    try {
      return evaluate(num, term);
    } catch (err) {
      //Now we are handling the error, but we find ourselves console logging again
      console.error('Error evaluating term', term, err);
      //Or stranger, throwing the error again, to be caught further up in our program
      throw new Error(err);
    }
    ...
  }, 0);
  ...
  return result;
}

Enter fullscreen mode Exit fullscreen mode

This is starting to feel like the race-track that keeps getting longer the further you run.

Here are a couple of routes you might want to take from here:

  • A. Fix the application logic higher up in the chain, so that the error never occurs.
  • B. Just tell the end user something went wrong, but let your application continue working because now the error is tucked away in a message.

In our calculator example, that becomes a choice of A, handle the error as soon as an invalid input is detected e.g.

if(previousTerm === 'divide' && evaluateExpression(currentTerm) === 0){
  writeToDisplay("Cannot divide by 0");
}
Enter fullscreen mode Exit fullscreen mode

or B, appreciate that you cannot anticipate all errors, and just use try { ... } catch

try {
  return evaluateTerm(term);
} catch (err) {
  writeToDisplay(err.message);
}
Enter fullscreen mode Exit fullscreen mode

The problem with B is that the error messages may be more generic, and might make it harder for the user to know what they did wrong, if the underlying throw new Error(message) was written poorely.

In the case of err.message, this would be coming from a throw statement inside evaluateTerm. In fact, we could combine A and B, giving us something more like this:

//throw errors in the self contained internal functions
function evaluateTerms(terms, previousTerm, currentTerm){
  ...
  if(previousTerm === 'divide' && evaluateExpression(value, currentTerm) === 0){
    throw new Error("Cannot divide by 0.");
  }
  ...
}
...
//display the error message to the user in a friendly way
//in the higher-level usage of these functions
try {
  terms.reduce((acc, term, index, terms) => {
    return evaluateTerms(terms, index > 0 ? terms[currentTerm-1] : null, term);
  }, 0);
} catch (err) {
  writeToDisplay(err.message);
}
Enter fullscreen mode Exit fullscreen mode

writeToDisplay would be the part of your application that actually outputs a message to your user's screen.

Conclusion
The idea is to throw errors in the smaller self-contained pure functions, or building blocks of your program, and to then catch these thrown errors at a higher abstraction level of your program, where these functions are being used to achieve the logic of your application. If the error is a result of a logic issue in your code, instead of an issue with user input, you will be able to more easily identify and debug the problem code as you will have thrown special Error objects which give you a stack trace in the console. In the case of invalid user input, you will display a message or some UI to indicate that their input is invalid.

Discussion (0)