In this tutorial, we will take a look at JavaScript Promise, how it works and how to use it with a few code examples.

What are Promises in JavaScript?

As you may already know, Javascript and Node.js are non-blocking IO (Input/Output). Simply put, they don’t wait for an input or output operation. Javascript will simply continue with the code in the event loop if it sees an IO event such as an HTTP request to a server or a file read from the disk in Node.js. This is the same for interval-based events as well.

Before Promises, JavaScript used something known as “callbacks” to handle asynchronous events. Developers were not a huge fan of callbacks due to various problems associated with it such as the callback hell which makes the code unorganized and unreadable.

Promises were introduced in Javascript EcmaScript 2015 (ES6). A Promise does not return a value immediately. Instead, it returns the result or throws an error after execution has been completed.

Promises are also eager, which means that they’ll start doing the task without waiting as soon as a Promise is created.

How Promises Works

First, let’s take a simple example of an asynchronous event in Node.js. Let’s read a file and print the output to the console.

const fs = require('fs');

fs.readFile('something.txt', 'utf8', (err, data) => {
    console.log(data);
});

console.log('This is the last line of the code');

If you run this code, you should see the following output,

This is the last line of the code
Whatever the content of the file

A file read is an IO event in Node.js. The process is required to wait until the file content is loaded into the memory from the disk before it can be used in the code.

In the above example, we have passed a callback as the final parameter which will be executed after the file has been read and the content is available to use.

On the other hand, a promise is an object which will be returned from an asynchronous function such as during a file read operation.

A Promise can have one of the three possible states at any time:

  1. Fulfilled – The operation has finished and results have been returned.
  2. Rejected – The operation has been rejected and an error was returned.
  3. Pending – The operation is still in progress.

Let’s rewrite our previous code example using promise.

const fs = require('fs');

const readFile = (fileName, encoding) => {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, encoding, (err, data) => {
            if (err) {
                reject(err);
                return;
            }

            resolve(data);
        });
    });
};

readFile('something.txt', 'utf8')
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.error(err);
    })
    .finally(() => {
        console.log('This will run no matter what')
    });

In this example, we have written a reusable method called readFile which reads files using promises instead of callbacks. As mentioned earlier, the readFile method returns a Promise object.

When creating a new Promise, we pass a function to the Promise constructor. It takes two parameters which are resolve() and reject(). After the operation is successful, we call the resolve() method and pass the result to it. If there’s an error, we call the reject() method and pass the error to it

In the given example, the promise will be in a ‘Pending’ state while the file is being loaded from the disk to the memory. After the read operation has been completed, the then() block will be called.

Also, the finally() callback method will always execute regardless of the result of the file reading operation.

Promise Chaining

With promise chaining, we can chain a few promises to run one after another where every Promise will be dependent on the previous promise in the chain.

In this example, we will use a promise chain to read a file and process its content, and then write a new file to the disk containing the content that was read from the input file.

const processFileContent = (content) => {
    return new Promise((resolve, reject) => {
        if (!content) {
            reject(new Error('Nothing in the file'));
            return;
        }

        resolve(content + 'We have added more content');
    });
};

const saveFile = (processedContent) => {
    return new Promise((resolve, reject) => {
        fs.writeFile('newFile.txt', processedContent, (err) => {
            if (err) {
                reject(err);
                return;
            }

            resolve();
        });
    });
};

readFile('something.txt', 'utf8')
    .then(processFileContent)
    .then(saveFile)
    .catch((err) => {
        console.log('Something was error');
    });

Executing Multiple Promises at the Same Time

In the previous example, the second promise is executed once the first one has ended. In this case, the problem is that the program has to wait until one promise is fulfilled before it can begin executing the next promise in the chain.

There can be situations where you want the promises to be independent of one another, such as reading multiple files from the disk. To achieve this, we can use the Promise.all method which returns a single promise that fulfills after all of the promises have been fulfilled. It accepts an array of promises and returns an array of results from the passed promises.

Here is an example of reading multiple files with Promise.all,

const promise1 = readFile('file1.txt', 'utf8');
const promise2 = readFile('file1.txt', 'utf8');
const promise3 = readFile('file1.txt', 'utf8');

Promise.all([promise1, promise2, promise3])
    .then(fileContents => {
        console.log('Content of the file1.txt', fileContents[0]);
        console.log('Content of the file2.txt', fileContents[1]);
        console.log('Content of the file3.txt', fileContents[2]);
    });

Since all of those promises are executed asynchronously, there will be multiple disk reads at the same time and each operation may take a different amount of time to finish. For example, the first promise might be the last one to finish because it contains more content to read than the other files. As such, the then() block will only execute once all the promises have finished execution.

Conclusion

Promises have become one of the most important features of Javascript in the last few years. Most modern JavaScript libraries take advantage of promises, such as AJAX.

We hope this tutorial has provided you with a good understanding of Promises in JavaScript. If you have any questions, feel free to let us know in the comments section below.