DEV Community

loading...
Cover image for Node.js & the Promise That Callback Hell Can Be Avoided

Node.js & the Promise That Callback Hell Can Be Avoided

jessrichmond profile image jessrichmond ・4 min read

Last week we saw an introduction to databases and servers where we were introduced to Node.js - an open source runtime envrionment that provides Javascript web developers with tools to talk to networks, file systems, and other input/output (I/O) sources.

We learned that one of the key features of Node.js is the ability to run functions asynchronously. Whereas the vanilla Javascript training wheels we’ve been rolling often had us reading a file of code from top to bottom without moving on to the next function until the current function is finished running, Node.js gives us the opportunity to work on a bunch of different functions all at the same time.

Real World Multitasking ≈ Async Javascript

Let’s look at a real world example that highlights the importance of this sort of multitasking: going to the DMV. It’s almost always an objectively unpleasant experience, but can you imagine how much more awful it would be if the DMV did not sort folks into categories and instead just took care of people in the order that they arrived? When some people come into the DMV they are there to get a driver’s license for the first time, register their vehicle, get temporary tags, etc! Each of these services takes a different amount of time to execute, so instead of making everyone wait in relation to the order in which they arrived, the DMV recognizes that it’s more efficient to first categorize the services they provide and then give people a place in line based on what they came in for.

Node.js does almost this exact same thing, except in a Javascript file. Instead of reading lines of code from top to bottom and waiting for one line to finish executing before moving on to the next, Node.js does not block the request thread and instead operates asynchronously.

Non-blocking Functions & Callbacks

Callbacks are what allow Node.js to operate this way; they’re used when doing any sort of I/O service ie downloading files, reading files, talking to databases. Our vanilla Javascript training has introduced us to functions that immediately return a result.

const greeting = () => {
  console.log('hello!');
}

greeting(); // 'hello!'

Functions that use callbacks, however take some time to produce a result.

const greeting = 'hello,';

const personalizedGreeting = (greeting, callback) => {
  if (err) {
    console.log('oops, an error.');
  } else {
    callback(greeting);
  }
};

personalizedGreeting(greeting, hiBetty);

function hiBetty (err, message) {
  if (err) {
    console.log('there is an error...');
  } else {
    console.log(`${message} betty the dog!`);
  }
};

Important to understanding Node.js is understanding that functions are allowed to jump around based on completion. In the above example, the hiBetty function is declared & hoisted (that is why we will not get a reference error); the personalizedGreeting function is invoked and passed getName as its callback; once getName has completed, the interpreter will print to the console, “hello Betty the dog”.

Entering the Realm of Callback Hell

Now, this is a pretty readable code but I/O operations are not usually this simple. Let’s say that instead of simply printing to the console when Betty the dog logs in, I want to check if her online portal is complete? Did she sign up with a name, a photo, her health records, and daily schedule? Let's see...

const profileComplete = (dog, callback) => {
  // is name attached to profile?
  isNameLoaded(dog, (err, loaded) => {
    if (err) {
      callback(err, null);
    } else if (!loaded) {
      callback('enter your name!', null);
    } else {
      // if name is attached, is photo attached?
      isPhotoUploaded(dog, (err, uploaded) => {
        if (err) {
          callback(err, null);
        } else if (!uploaded) {
          callback('upload a photo!', null);
        } else {
          // are health records online?
          checkHealthRecords(dog, (err, available) => {
            if (err) {
              callback(err, null);
            } else if (!available) {
              callback('please upload your health records', null);
            } else {
              // is daily schedule online?
              isScheduleAttached(dog, (err, attached) => {
                if (err) {
                  callback(err, null);
                } else if (!attached) {
                  callback('add your daily schedule!', null);
                } else {
                  callback(null, attached);
                }
              })
            }
          })
        }
      })
    }
  })
}

This code hurts the eyes and writing this code hurt the brain. Do you see how the code gradually extends to the right? This is what ppl who code affectionately call the pyramid of doom aka the defining characteristic of 🔥 callback 🔥 hell 🔥.

There are a handful of ways to get around callback hell. You can do as callbackhell.com suggests: keep your code shallow, modularize your functions, and handle every single error. The other option is to level UP and learn how to implement 🤞🏼 promises 🤞🏼.

Making Promises in Async Functions

Introduced in ES6, promises are Javascript objects that link producing code and consuming code. With promises, there’s an agreement that one function will execute once another function has been completed.

const funcName = new Promise((resolve, reject) => {...});

The function that is passed to a new Promise is an “executor”… it encapsulates a function that will eventually return a result. When the executor returns the result, it will then call ‘resolve’ with the value or “reject” with an error. If the executing function resolves to a value rather than being rejected as an error, its return value can be registered using handlers such as ‘.then’, ‘.catch’, and ‘.finally’; ‘.then’ continues on the passing along of resolved values, ‘.catch’ takes care of any errors that may pop up, and ‘.finally’ is a good for doing cleanup as it’ll stop any loading indicators.

Now let’s take a look at our previous example but refactored to include promises.

const profileComplete = (dog) => {
  return isNameLoaded(dog)
    .then((dog) => {
      return isPhotoUploaded(dog);
    })
    .then((dog) => {
      return checkHealthRecords(dog);
    })
    .then ((dog) => {
      return isScheduleAttached(dog)
    })
    .catch((err) => {
     return console.log('an error has occurred.')
    });
};

See, way easier to follow along! Promises are maybe, at this point of our coding journey, not the most intuitive functions to construct but it’s important to begin getting in the habit of making code more universally understandable as it is teamwork that makes the dream work.

Discussion

pic
Editor guide