Three Ways to Improve Error Handling in Node.js

Three Ways to Improve Error Handling in JavaScript

Learn three ways to make your code more resilient with error handling in JavaScript.

Jieun Kim | December 9, 2019

Error handling can sometimes feel like a chore; but as a developer, if there’s one thing you know, it’s a matter of when errors will occur, not if. In this blog post, we’ll take a look at three ways you can improve error handling in JavaScript, increasing your peace of mind and confidence in your code. 

Use Promises or Async / Await for async error handling

If you’ve written any amount of asynchronous JavaScript code, you’ve probably heard of Callback Hell, if not experienced it first-hand yourself (likely when inheriting some legacy code you’ve had to parse or fix).  Callbacks don’t scale well due to excessive nesting which makes the code hard to read, maintain, and reason about when it comes to code flow. Happily, both Promises or Async / Await exist to make asynchronous requests more manageable, especially when it comes to error handling. 

In order to better appreciate Promises and async / await, let’s first take a look at an example of the infamous Christmas tree shape “Callback Hell” takes: 

node js callback hell

What’s happening here? Functions are nested in another function;  each function gets an argument, which is another function that is called with a parameter that is the result of the previous function. Trying to debug and reason about error handling flow with callbacks is extremely difficult and prone to error itself. Callback hell occurs when developers try to write JavaScript in a way where execution happens visually from top to bottom. Since asynchronous code involves functions that don’t return a result right away, each nested function is waiting on results from a previous function that takes a while to get a result, and timing all this right is extremely confusing.

The syntax with Promises is more elegant. A Promise is a “proxy for a value not necessarily known when it is created” and this lets “asynchronous methods return values like synchronous methods”. The .then and .catch methods return promises, so they can be chained together. Catching errors with Promises might look something like this: 

fetch('coffee.jpg') 

  .then(response => response.blob()) 

  .then(myBlob => { 

    let objectURL = URL.createObjectURL(myBlob); 

    let image = document.createElement('img'); 

    image.src = objectURL; document.body.appendChild(image); 

}) 

  .catch(e => 

    { console.log('There has been a problem with your fetch    

operation: ' + e.message); 

});

With async/await, the syntax is even more succinct; it implicitly uses a Promise to return a result but the syntax and structure make it readable and makes it much more like using standard synchronous functions; this means catching errors may look something like this with a good old try/catch block:

async function myFetch() { 

  try { 

    let response = await fetch('coffee.jpg'); 

    let myBlob = await response.blob(); 

    let objectURL = URL.createObjectURL(myBlob); 

    let image = document.createElement('img'); 

    image.src = objectURL; document.body.appendChild(image); 

  } catch(e) { 

    console.log(e); 

  } 

} 

myFetch();

 

Use a persistent logging framework to maximize the visibility of errors  

We know and love console.log as a quick way to see what’s happening in our code, but using it as the only way to track your errors gets unwieldy fast as your code grows more complex. Mature loggers for JavaScript can help you effectively collect pertinent information about errors and gives you the ability to query errors, among other things. winston and pino are two popular npm libraries that have been around for years and work in tandem with your code to improve error handling. Let’s take a look at a few advantageous features winston provides:

winston is a logging library with support for multiple transports, each transport essentially being a storage device for your logs.  Core transports included in winston leverage the built-in networking and file I/O offered by Node.js core. As the documentation explains, “Each instance of a winston logger can have multiple transports configured at different levels.” Multiple transports are useful for when you want logs to output to different places. For example, “one may want error logs to be stored in a persistent remote location (like a database), but all logs output to the console or a local file.” What’s more, transports can be customized to suit your use case. For example, you can define the logging level of each transport to determine which logging messages will be displayed, so that you’re not indiscriminately flooded with all notifications at once. 

winston also enables you to enable timestamps to appear in log entries by utilizing formats, prototyal objects in winston, like so: 

const { createLogger, format, transports } = require('winston');

const { combine, timestamp, label, printf } = format;




const myFormat = printf(({ level, message, label, timestamp }) => {

  return `${timestamp} [${label}] ${level}: ${message}`;

});




const logger = createLogger({

  format: combine(

    label({ label: 'right meow!' }),

    timestamp(),

    myFormat

  ),

  transports: [new transports.Console()]

});

This comes in handy not only for knowing when errors were thrown but serves as a useful piece of information you can utilize when you find yourself needing to query errors that occurred within a certain timeframe. 

If what you care most about is speed, however, pino is beloved for being the fastest logger for Node.js applications – their documentation includes a benchmarks section that compares averages across several popular logging frameworks. pino logging also includes things like timestamps, to know when things happened and process IDs, in case you are running multiple node processes. 

Testing error handling flow using a flexible test framework

Now that you’ve made error handling improvements in your code with await / async, Promises and mature loggers, you’ll want to make sure that your error handling flow is, in fact, working the way that you planned. Flexible test frameworks like Jest support exception testing – it’s important to integrate this into your workflow. Otherwise, you can’t be certain that exceptions are being handled correctly.  Jest makes it easy by allowing you to use async and await in your tests. Here’s an example of how you might do that:

test('the data is peanut butter', async () => {

  const data = await fetchData();

  expect(data).toBe('peanut butter');

});




test('the fetch fails with an error', async () => {

  expect.assertions(1);

  try {

    await fetchData();

  } catch (e) {

    expect(e).toMatch('error');

  }

});

As you can see,  using await / async and Promises makes it easier to reason about the flow of your code, which is critical when tracking down errors and maintaining your code over time. Using mature loggers helps you to gain greater control and helps you to pinpoint where it is that errors occur, the first step in rectifying said errors. And using frameworks like Jest make sure that the error handling flow is happening as it ought. Now, if you’d like to get started on a Node.js project and try integrating these practices into your own code, check out our Node SDK quickstart guide!

About the Author

jieun kim is a technical writer based in nyc. in her spare time, she enjoys contemplating shapes of all kinds and accumulating large stacks of books.

Ready to Start Building?