In last section, we saw that many I/O operations take a callback function to report the result (if successful) or the error (if failing).
import dns from 'dns';
const govHost = "www.gov.mo";
dns.resolve(govHost, (err, records) => {
if (err) throw err;
console.log(`${govHost} resolves to ${records}`);
});
These asynchronous callbacks are efficient and easy to understand. However, when we need to organize a sequence of asynchronous operations, excessive nesting of the callback functions will soon make the code overwhelming.
Modern JavaScript introduces Promise, an abstraction to model the result and error from an operation that will complete in the future. According to MDN article - Using promises,
A Promise is an object representing the eventual completion or failure of an asynchronous operation.
Below is a rewrite of the DNS resolve example using promises.
// p121-promdns.mjs
import { promises } from 'dns';
const resolver = new promises.Resolver();
const govHost = "www.gov.mo";
resolver.resolve(govHost).then( (records) => {
// succeed
console.log(`${govHost} resolves to ${records}`);
}).catch( (err) => {
// fail
console.log(`Resolve fails with ${err}`);
})
The line resolver.resolve(govHost)
creates a Promise
object.
A promise has both a state and a result. At start, the program is waiting for a reply from a name server, and the promise is said to be in the pending
state, and have a undefined result. After some time, the program may receive DNS records successfully, and the state of the promise changes to fulfilled
, and the result contains the resolution records. On the other hand, if the DNS resolution fails, the state of the promise becomes rejected
.
Since a promise represents the result/error of a task to be completed, when its state changes from pending
to fulfilled
or rejected
, it will never change again. The promise is said to be settled
. We also say resolve a promise to refer to the action that the promise becomes fulfilled
with a result, and reject a promise to refer to that action that the promise becomes rejected
with an error.
Please refer to the state diagram in the online reference promise basics for more information about the three states of a promise.
Also try the promise dice example to examine state change of a promise in a web browser.
The result is not a property of the promise object. To access the result/error of a promise object, use the .then()
or .catch()
methods.
The full form of .then()
has two parameters: one is the callback when the promise is resolved (successful completion), and the other is the callback when the promise is rejected (failed completion). Try the examples p122-success.mjs
and p122-failure.mjs
.
// p122-success.mjs
import { promises } from 'dns';
const resolver = new promises.Resolver();
const host = "www.gov.mo";
const prom = resolver.resolve(host);
prom.then(
rec => { console.log(`${host} resolves to ${rec}`) }
,
err => { console.log(`Name server returns an error: ${err}`) }
);
If you’re only interested in the result in successful completion, you can use .then()
with one callback only. The callback is sometimes known as then handler.
prom.then(rec => {
console.log(`${host} resolves to ${rec}`)
})
On the other hand, if you’re only interested in the error object in case of failure, you can use .catch()
with one callback. The callback is sometimes known as catch handler.
prom.catch(err => {
console.log(`Name server returns an error: ${err}`)
})
Also try to add handler to the promise dice.
We often need to perform I/O operations one after another. For example, to copy a file, we need to call readFile
to get the file content, and then call writeFile
to write the content to a copy.
In the last section, we use nested callbacks to do this. Now, let’s examine the promise versions of readFile
and writeFile
. Both functions return promises.
readFile('mpi-info.txt', 'utf8').then( data=>{
// data contains content of the input file
console.log(data);
});
let data = "content read earlier";
writeFile('copy-mpi-info.txt', data, 'utf8').then( ()=>{
console.log("Finish copying");
});
The method .then()
returns another promise object, and it allows us to chain .then()
together in the form prom.then().then().then()
. What promise the .then()
method returns depends on the inner working of the handler.
.then()
also returns the same promise..then()
returns a fulfilled promise with the return value as result..then()
returns a rejected promise with the exception object as error.With this in mind, we can rewrite the file copy example in the last section using promises.
// p123.mjs
import { readFile, writeFile } from 'fs/promises';
readFile('mpi-info.txt', 'utf8').then( data=>{
// data contains content of the input file
return writeFile('copy-mpi-info.txt', data, 'utf8')
}).then( ()=>{
console.log("Finish copying");
})
In a promise chain prom.then().then()
, if any handler throws an exception (which is wrapped up as a rejected promise) or any then()
returns a rejected promise, the chain will skip the remaining then()
and run the nearest catch()
. Therefore, we can handle the errors in a promise chain by adding a catch()
handler near the end of the chain.
// p124.mjs
import { readFile, writeFile } from 'fs/promises';
readFile('non-exist-mpi-info.txt', 'utf8').then( data=>
writeFile('copy-mpi-info.txt', data, 'utf8')
).then( ()=>{
console.log("Finish copying");
}).catch( err=>{
console.error("promise catch "+err);
})
Test error handling of promise in promise dice.
Modern JavaScript provides two keywords await
and async
then make using promises even easier.
Given a promise prom
, you can wait and obtain its result with let result = await prom
. If the promise is rejected, then the await statement throws an exception. Essentially, await
makes promises work like synchronous I/O functions in Java or Python. Below is a rewrite of p124.mjs (file copy) using await
.
// p125.mjs
import { readFile, writeFile } from 'fs/promises';
try {
let data = await readFile('mpi-info.txt', 'utf8');
await writeFile('copy-mpi-info.txt', data, 'utf8');
console.log("Finish copying");
} catch (err) {
console.error("promise catch "+err);
}
However, when I say await
will wait for the fulfillment or rejection of a promise, you should be aware that in JavaScript, blocking is generally undesirable. JavaScript has only one thread, and when that single thread is blocked, the whole program is blocked and cannot process any network services or interaction in user interface. The example p123.mjs
demonstrates an exception, not a norm, of JavaScript. You can use await
on the top-level statements, which may block the single thread. However, you cannot block the event loop.
JavaScript does not allow you simply use await
inside a ‘normal’ function. You can only use await
inside a function that is marked as async
(asynchronous).
// p126.mjs
import { readFile, writeFile } from 'fs/promises';
async function copyFile (sourceFileName) {
try {
let data = await readFile(sourceFileName, 'utf8');
await writeFile('copy-' + sourceFileName, data, 'utf8');
console.log("Finish copying");
} catch (err) {
console.error("promise catch "+err);
}
}
await copyFile('mpi-info.txt');
Essentially, async function
returns a promise object. It may take a long time to finish running the function, but when it returns, it can return a value (i.e. result). If an exception is thrown inside an async function
, the function stops execution, and the returned promise object is rejected with the exception object. The following example rewrites p126.mjs
to illustrate this.
// p127.mjs
import { readFile, writeFile } from 'fs/promises';
async function copyFile (sourceFileName) {
let data = await readFile(sourceFileName, 'utf8');
await writeFile('copy-' + sourceFileName, data, 'utf8');
}
copyFile('mpi-info.txt').then( () => {
console.log("Finish copying");
}).catch(err => {
console.error("promise catch "+err);
});
The example source files are available here.