A Promise
is a promise from the producing code to the consuming code that I (the producing code) will return something that you (the consuming code) can work with.
The producing code is often something that takes time, so it is usually written in an asynchronous manner, allowing it to be executed in parallel with other less time-consuming tasks. The consuming code is something that needs to use the result of the producing code once it is ready.
The promise is what links them together. This is important because in practice, you cannot expect the producing code to always be successful. It is possible that the time-consuming task eventually fails and returns something that the consuming code cannot work with.
Compared to the callback style syntax we've seen in the previous lesson, promises offer a cleaner and more intuitive way of writing asynchronous programs.
The Promise constructor
A promise is created with a Promise
constructor, which has the following syntax:
1const promise = new Promise((resolve, reject) => {
2 . . . // Producing code
3});
The Promise
constructor accepts a function argument, which contains the producing code. The function takes two callbacks as the input, resolve()
and reject()
.
The producing code will be executed immediately when new Promise()
is created, and if it gives the right outcome, resolve()
should be executed. And if something goes wrong, reject()
should be executed. For example,
1const promise = new Promise((resolve, reject) => {
2 const rand = Math.random() * 100;
3
4 if (rand >= 50) {
5 resolve(rand);
6 } else {
7 reject(new Error("Random number is less than 100"));
8 }
9});
In this case, Math.random()
generates a random number between 0 and 1, so rand
should be a random number between 1 and 100.
If rand
is greater than or equal to 50, the promise is considered successfully resolved, and resolve()
will be executed. If not, the promise is not successfully resolved, and reject()
will be executed.
Resolving a promise
The Promise()
constructor creates an object with its state
property equals to "pending"
, after the promise has been resolved, it will become "fulfilled"
.
To fulfill a promise, you need to connect the producing code with the consuming code using then()
.
1promise.then(
2 (result) => {
3 console.log(`Promise fulfilled: ${result}`);
4 },
5 (error) => {
6 console.error(`Promise rejected: ${error}`);
7 }
8);
The method then()
accepts two arguments. If the producing code is successful, the first argument will be executed as resolve()
. For example, in case of success, resolve(rand)
will be executed. The value of rand
will be passed to the argument result, and becomes:
1(rand) => {
2 console.log(`Promise fulfilled: ${rand}`);
3};
Which gives something like:
1Promise fulfilled: 86.03118001726733
In case of failure, the second argument will be executed as reject()
. new Error(. . .)
will be passed as error
, and produce:
1Promise rejected: Error: Random number is less than 100
2 at /Users/. . ./index.js:149:12
3 . . .
Try the code example on your computer a few times, and it should give you both results.
The second argument is optional for the then()
method. You can choose to handle only the success case, and then chain another catch()
method to handle the unsuccessful case.
1promise
2 .then((result) => {
3 console.log(`Promise fulfilled: ${result}`);
4 })
5 .catch((error) => {
6 console.error(`Promise rejected: ${error}`);
7 });
This makes a slightly cleaner syntax.
Even though our previous example demonstrates how a promise works, it is not very practical. Because the producing code is usually asynchronous. It should be something that is time consuming, but our example is synchronous and rand
will be created almost instantly without any delay.
So, here is a more realistic example:
1const success = true;
2
3const promise = new Promise((resolve, reject) => {
4 // Simulating a time-consuming task
5 setTimeout(() => {
6 if (success) {
7 resolve("Task finished.");
8 } else {
9 reject(new Error("Task failed."));
10 }
11 }, 1000);
12});
13
14promise
15 .then((result) => {
16 console.log(`Promise fulfilled: ${result}`);
17 })
18 .catch((error) => {
19 console.error(`Promise rejected: ${error}`);
20 });
Here, we are using setTimeout()
to simulate a time-consuming task, and success
indicates the status of the task. If success
is true, the promise will be resolved, and if success
is false, the promise will be rejected.
Why use a promise
So why use a promise at all? Remember that previously we are using setTimeout()
directly, but now we have to wrap it inside a promise. Seems like we are only making things more complex here.
The whole point of using a promise is to turn an asynchronous operation that requires callback functions into a promise-based syntax that looks like:
1promise
2 .then((result) => {
3 console.log(`Promise fulfilled: ${result}`);
4 })
5 .catch((error) => {
6 console.error(`Promise rejected: ${error}`);
7 });
This process is known as promisification. For example, previously, we are reading a file like this:
1fs.readFile("file.txt", "utf8", function (err, data) {
2 console.log(data);
3});
To promisify this readFile()
function, this is what we can do:
1function read(filePath) {
2 return new Promise((resolve, reject) => {
3 fs.readFile(filePath, "utf8", (err, data) => {
4 if (err) {
5 reject(err);
6 } else {
7 resolve(data);
8 }
9 });
10 });
11}
Remember that the producing code will be executed immediately when new Promise()
is created. However, in this case, you might not want to do that because the producing code is often time-consuming. So instead, we are making a function read()
to return new Promise()
. This way, the producing code will only be executed when we call read()
.
And now, the old school callback syntax can be rewritten into:
1read("./file.txt").then(console.log).catch(console.log);
Promise chaining
The promise enables you to turn operations that usually require multiple callbacks into a chain of then()
methods. For example, normally, if you need to read multiple files, you'll need to use readFile()
like this:
1// Read first file
2fs.readFile("file1.txt", "utf8", function (err, data) {
3 console.log(data);
4});
5
6// Read second file
7fs.readFile("file2.txt", "utf8", function (err, data) {
8 console.log(data);
9});
10
11// Read third file
12fs.readFile("file3.txt", "utf8", function (err, data) {
13 console.log(data);
14});
But with the promise syntax, you could make the then()
method return another promise:
1read("./file1.txt")
2 .then((data) => {
3 console.log(data);
4 return read("./file2.txt"); // Return another promise to read the second file
5 })
6 .then(. . .);
The second promise will be caught by the next then()
, where you can process the second file, and then return another promise for the third file.
1read("./file1.txt")
2 .then((data) => {
3 console.log(data); // Print the first file
4 return read("./file2.txt"); // Promise the second file
5 })
6 .then((data) => {
7 console.log(data); // Print the second file
8 return read("./file3.txt"); // Promise the third file
9 })
10 .then((data) => {
11 console.log(data); // Print the third file
12 });
1This is file number 1
2This is file number 2
3This is file number 3
But, what if there is an error? By default, the execution of the chain will stop when an error is encountered. For example, in the first then()
method, let's return a promise for a file that does not exist.
1read("./file1.txt")
2 .then((data) => {
3 console.log(data); // Print the first file
4 return read("./notexist.txt"); // Promise the second file, which does not exist
5 })
6 .then((data) => {
7 console.log(data); // Print the second file
8 return read("./file3.txt"); // Promise the third file
9 })
10 .then((data) => {
11 console.log(data); // Print the third file
12 });
1This is file number 1
2node:internal/process/promises:289
3 triggerUncaughtException(err, true /* fromPromise */);
4 ^
5
6[Error: ENOENT: no such file or directory, open './notexist.txt'] {
7 errno: -2,
8 code: 'ENOENT',
9 syscall: 'open',
10 path: './notexist.txt'
11}
12
13Node.js v21.6.0
JavaScript stops at the point where the error occurs, and the third file will not be promised.
To solve this problem, make sure you catch()
the error at every then()
. This gives JavaScript a way to deal with errors, instead of causing the entire program to crash.
1read("./file1.txt")
2 .then((data) => {
3 console.log(data); // Print the first file
4 return read("./notexist.txt"); // Promise the second file
5 })
6 .catch(console.log) // Catch the error
7 .then((data) => {
8 console.log(data); // Print the second file
9 return read("./file3.txt"); // Promise the third file
10 })
11 .catch(console.log) // Catch the error
12 .then((data) => {
13 console.log(data); // Print the third file
14 })
15 .catch(console.log); // Catch the error
1This is file number 1
2[Error: ENOENT: no such file or directory, open './notexist.txt'] {
3 errno: -2,
4 code: 'ENOENT',
5 syscall: 'open',
6 path: './notexist.txt'
7}
8undefined
9This is file number 3
This time, even though the second file still does not exist, JavaScript simply catches the error and logs the message to the console. The chain does not break in this case.
Rewrite compareFiles()
Going back to our previous lesson, and remember that we created a compareFiles()
using two nested readFile()
functions.
1function compareFiles(f1, f2) {
2 fs.readFile(f1, "utf8", (error, data1) => {
3 fs.readFile(f2, "utf8", (error, data2) => {
4 if (data1 === data2) {
5 console.log("Files are the same.");
6 } else {
7 console.log("Files are not the same.");
8 }
9 });
10 });
11}
This example only compares two file, and it is already difficult to read and understand. As we compare more and more files, this nested callbacks syntax will become a mess. So instead, let's see if we can recreate it using promises.
First of all, we need to promisify the readFile()
function.
1function read(filePath) {
2 return new Promise((resolve, reject) => {
3 fs.readFile(filePath, "utf8", (err, data) => {
4 if (err) {
5 reject(err);
6 } else {
7 resolve(data);
8 }
9 });
10 });
11}
As for compareFiles()
, things get complicated.
1function compareFiles(f1, f2) {
2 read(f1).then((data1) => {
3 read(f2).then((data2) => {
4 // Compare data1 and data2 here
5 });
6 });
7}
Because we'll need to compare data1
and data2
, and with what we've learned so far, it seems like the only way to have access to both of them is to read f1
first to get data1
, and then inside the then()
method, read f2
to get data2
.
This is not an elegant solution at all. We are still using the nested structure we are trying to get rid of.
Another way to do this is to use Promise.all()
, which is a static method under the Promise
constructor. This method allows you to handle multiple promises together. For example:
1function compareFiles(f1, f2) {
2 return Promise.all([read(f1), read(f2)])
3 .then(([data1, data2]) => {
4 if (data1 === data2) {
5 console.log("Files are the same.");
6 } else {
7 console.log("Files are not the same.");
8 }
9 })
10 .catch((error) => console.error("Error reading files:", error));
11}
read(f1)
returns the promise for the first file, and read(f2)
returns the promise for the second file. The two of them are put together in an array. The following then()
method deals with both of the parsed file data, data1
and data2
, which are also put in a array.
If you need to compare more files, simply add more promises in the array.
1function compareFiles(f1, f2, f3) {
2 return Promise.all([read(f1), read(f2), read(f3)])
3 .then(([data1, data2, data3]) => {
4 if (data1 === data2 && data2 === data3) {
5 console.log("Files are the same.");
6 } else {
7 console.log("Files are not the same.");
8 }
9 })
10 .catch((error) => console.error("Error reading files:", error));
11}
12
13compareFiles("./file1.txt", "./file2.txt", "./file3.txt");
Compared to the previous compareFiles()
, this new syntax is much easier to read and maintain.