DEV Community

Cover image for Understanding Callbacks and Promises
Fernando Hernandez
Fernando Hernandez

Posted on

Understanding Callbacks and Promises

These two concepts are basic things of the Javascript programming language. Because this language works under the paradigm of asynchronous programming.

So, I decided to share this article to give a sense to what callback and promises are. Two characteristics to carry out asynchronous operations.

So, let's go 👍

Callbacks

To understand the callback I will make a brief analogy.

Suppose we are talking on the phone. When talking, a situation arises to resolve immediately. We put the call on hold, we do what we have to do and when we finish, we return to the call that we left on hold.

Well, simply with this example we can give us an idea in general, what is a callback. Basically, as the name says it is.

Now, speaking in programming language.

A callback is a function that will be executed when an asynchronous operation has been completed.

A callback is passed as an argument to an asynchronous operation. Normally, this is passed as the last argument of the function. Doing this it’s a good practice, so keep that in mind.

The callback structure seems like this:

function sayHello() {
    console.log('Hello everyone');
}

setTimeout(sayHello(), 3000)

What we did in the above example was, first to define a function that prints a message to the console. After that, we use a timer called setTimeout (this timer is a native Javascript function). This timer is an asynchronous operation that executes the callback after a certain time. In this example, after 3000ms (3 seconds) will be executed the sayHello function.

Callback Pattern

As we mentioned at the beginning, as great developers we should respect the callback position as a parameter. Which should always be placed as the last one. This has for the name the callback pattern.

In this way, our code will be more readable and will be maintained more easily when other programmers work on it.

Let's see another callback example:

const fs = require('fs') // Importing Nodejs library

// Declaring file path
const filePath = './users.json'

// Asynchronous operation to read the file
fs.readFile(filePath, function onReadFile(err, result) {
    // In case of error print it in the console
    if (err) {
        console.log('There was an error: ' + err)
        return // Get out of the function
    }
    // Print on the console the file and the content of it.
    console.log('The file was successfully read it: ' + result)
})

Here, we are using a Nodejs library that is used to make operations on our file system. In the example, we are using the readFile function that works to read a file from our computer. This function receives two parameters (the file path and the callback). As we can notice, the callback named onReadFile it is found as the last parameter.

It is common for the callback to be declared anonymously, but it is good practice to assign a name to it, in case of an error, to identify it more easily.

Finally, that callback will be executed until our code finishes reading the requested file. Javascript will continue to execute code during this process if it exists.


Callback Hell

Once, you know how callbacks work and put in in practice, we have to keep in mind something. As a good developer, we have to know how to use it and avoid ugly things like the callback hell.

Callback hell is the misuse of callbacks. It looks like this:

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename,             function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

Basically, the use of nested callback is a bad practice and visually produces a kind of pyramid, as we can see. This becomes a difficult code to maintain and read and we do not want that.

How to avoid the callback hell?

  • Name functions: As I said before, the first thing that you can do is naming your functions (callbacks). Thus, when an error is generated, it will indicate the error in a specific way with the name of the function. Also, that you allow your code to be more descriptive and when other programmers read it, it is easier for them to maintain it.

  • Modularize: Once you have named your functions, you can start defining them by separate. This way, you will only put the callback name. First, start by defining them in the same file, at the bottom of your file. Then, another option is by writing that function on a separate file. That way, we can export and import it in whatever file.

This allows reusability of the code, greater readability, and easy maintenance.

  • Handle errors: When writing code, we must bear in mind that errors can always occur. To be able to identify them easily, it is very important to write code handling the errors that may happen.

In a callback, in general, errors are passed as the first parameter. We could handle an error in the following way:

const fs = require('fs')

const filePath = './users.json'

fs.readFile(filePath, handleFile)

function handleFile(err, result) {
    if (err) {
        return console.log('There was an error: ' + err)
    }
    console.log('File: ' + result)
}

Applying good code practices, makes the rest of the programmers do not hate you for the rest of your life!


Promises

Promises image

The promises in Javascript are just that, promises. We know that when we make a promise, it means that we will do everything possible to achieve the expected result. But, we also know that a promise cannot always be fulfilled for some reason.

Just as a promise is in real life, it is in Javascript, represented in another way; in code.

Let's see an example of a promise:

let promise = new Promise(function(resolve, reject) {
    // things to do to accomplish your promise

    if(/* everything turned out fine */) {
        resolve('Stuff worked')
    } else { // for some reason the promise doesn't fulfilled
        reject(new Error('it broke'))
    }
})

A promise is a native class of Javascript (since ES6).

The constructor of a promise receives an argument: a callback, which has two parameters:

  • resolve
  • reject

These are functions already defined in Javascript, so we should not build them ourselves.

This callback, which has these two functions as parameters, is called the executor.

The executor runs immediately when a promise is created.

What is this executor function going to execute?

Well, within this, we will put all the code necessary for our promise to be fulfilled.

Once the executor finishes executing, we will send one of the functions that it has as an argument.

  • In case it is fulfilled, we use the resolve function.

  • In case it fails for some reason, we use the reject function.

The functions resolve and reject, receive only one argument. It is common for the reject function to pass an error with the Error class, as we saw in the previous example.

Promises have three unique states:

  • Pending: The asynchronous operation has not been completed yet.

  • Fulfilled: The asynchronous operation has completed and returns a value.

  • Rejected: The asynchronous operation fails and the reason why it failed is indicated.

The promise object has two properties:

  • State: Indicates the state of the promise.
  • Result: Stores the value of the promise if it is fulfilled or the error if it is rejected.

Initially, the state of a promise is 'pending' and the result is 'undefined'.

Once the promise has finished its execution, the status and result of the promise will be modified to the corresponding value. Depending on whether the promise was completed or rejected.

Let's see the following graphs to understand it better:

Promise fulfilled

Promise rejected

Once the promises change their status, they can not be reversed.

How to consume or call a promise?

To consume a promise we have created, we use the then and catch functions. In code, they would look something like this:

promise.then(function(result) {
    console.log(result)
}).catch(function(err) {
    console.log(err)
})

The function then will allow us to handle the promises that are completed or that are fulfilled.

The function catch will allow us to handle the promises that are rejected.

In the then function, we can also handle the rejected promises. For this, the handler receives two arguments. The first will be in case the promise is fulfilled and the second in case it is rejected. In this way:

promise.then(function(result) { // Handling the value
    console.log(result)
}, function(err) { // Handling the error
    console.log(err)
})

The then and catch handlers are asynchronous.

Basically, then and catch will be executed once Javascript finished reading the code below.

Example:

promise.then(function(result) {
    console.log(result)
}).catch(function(err) {
    console.log(err)
})

console.log('Hello world')

We could think that first it will be printed in the value or error of the promise. But knowing that they are asynchronous operations, we have to keep in mind that it will take a minimum time to be executed, therefore the message "Hello world" is shown first.


The Promise class has a method called all, which is used to execute an array of promises. It looks something like this:

Promise.all([
    new Promise.((resolve, reject) => setTimeout(() => resolve(1), 3000)), // 1
    new Promise.((resolve, reject) => setTimeout(() => resolve(2), 2000)), // 2
    new Promise.((resolve, reject) => setTimeout(() => resolve(3), 1000)), // 3
]).then(result => console.log(result)) // 1, 2, 3

The then handler will print in console an array of the results of each promise.
If one of the promises is rejected, this function will be rejected with an error. As it is shown in the following image:

Promise.all([
    new Promise.((resolve, reject) => setTimeout(() => resolve(1), 3000)), // 1
    new Promise.((resolve, reject) => setTimeout(() => resolve(2), 2000)), // 2
    new Promise.((resolve, reject) => setTimeout(() => reject(new Error('An error has ocurred')), 1000))
]).then(result => console.log(result))
.catch(err => console.log(err)) // An error has ocurred

There is another method similar to all, but with a difference. It's the race method.

The same as the all function, it receives an array of promises, but it will return the promise that is completed or rejected first. Let's see an example of code:

let promise1 = new Promise(function(resolve, reject) {
    setTimeout(function() {
        resolve('promise one')
    }, 3000) // Resolve after 3 seconds
})

let promise2 = new Promise(function(resolve, reject) {
    setTimeout(function() {
        resolve('promise two')
    }, 1000) // Resolve after 1 seconds
})

Promise.race([
    promise1,
    promise2
]).then(result => console.log(result)) // promise two

As we can see, the value that returns to us is only the response of the second promise. This is because the second promise is executed first.
Let's see another example of a promise that is rejected:

let promise1 = new Promise(function(resolve, reject) {
    setTimeout(function() {
        resolve('promise one')
    }, 3000) // Resolve after 3 seconds
})

let promise2 = new Promise(function(resolve, reject) {
    setTimeout(function() {
        resolve('promise two')
    }, 2000) // Resolve after 2 seconds
})

let promise3 = new Promise(function(resolve, reject) {
    setTimeout(function() {
        reject('promise three rejected')
    }, 1000) // Reject after 1 second
})

Promise.race([
    promise1,
    promise2,
    promise3
]).then(result => console.log(result))
.catch(err => console.log(err)) // promise three is rejected

In this code, the race function, what is going to print is the error that it found in the third promise we declared. You can already imagine why. Effectively, the third promise is executed first than the others.

So, the race method, regardless of whether the promise is rejected or completed, will execute the first one and ignore the others.


Up to this point, I hope I have made myself understood about callbacks and promises. Basically, these two characteristics of Javascript are used to handle asynchronous operations. Which is what this language is based on and therefore its popularity.

I will continue with another article soon about the last functionality to handle asynchrony. Async-Await.

Top comments (5)

Collapse
 
thomasaudo profile image
thomasaudo

Great article, mastering promise is the real key for server-side operations

Collapse
 
_ferh97 profile image
Fernando Hernandez

Thank you so much man. I'm really glad to start contributing on this community

Collapse
 
faizanoor3001 profile image
Faiza Noor

Hey! Really liked your post, i have been working on Java and now making a move to JS , would like to know if there are any resources i can learn JS.
Lots of stuff is online and lots of confusion.
Thanks in advance. :)

Thread Thread
 
_ferh97 profile image
Fernando Hernandez • Edited

Thanks Glad you like it! Agree with you, lots of information online, we can find everything right now, the thing is to find the correct resources. There is a website called javascript.info, I suggest you to check out.

Collapse
 
aman81200 profile image
aman81200

I was very confused by looking at the other video and text resources available online.
but After reading this article all the confusion was cleared, This is the best article available for callbacks and promises.
Thank you so much :)