Asynchronicity

One of the hallmarks of Node.js is its wide support for asynchronous operations. Nearly every function that involves the file system, network, or user input will run asynchronously. The code right after the asynchronous code will run immediately, without waiting for the asynchronous function to complete its task, just as you saw with fetch in client-side scripts. When an asynchronous operation has a result, it sends the result to other code in one of several ways.

Callbacks

Suppose you are writing a utility to look up a server's IP address given its domain name. Finding this information usually requires a trip out to a server that maps domain names to IP addresses, so you expect Node.js to provide an asynchronous function for this. And it does. You find the resolve function in its DNS library.

Once the IP address is figured out, resolve calls the function that it was given and passes the IP address as a parameter. Based on the documentation, you write this code to look up the IP address of the domain name that is provided as a command-line argument:

const dns = require('dns');

const name = process.argv[2];
dns.resolve(name, (error, matches) => {
if (error) {
console.error(error);
} else {
console.log(matches[0]);
}
});

Try running this code, providing a domain name like example.com.

Callbacks are the simplest mechanism for making code dependent on an asynchronous operation. However, imagine if the callback above called its own asynchronous operation. You would have a callback inside a callback. Imagine if that second callback called yet another asynchronous operation. If you have a gauntlet of many asynchronous operations to execute, the code to respond to them gets very nested and messy.

In client-side JavaScript, you used callbacks with addEventListener. Your callback would be run whenever an event happened, and there were many possible events. In this example, there is only one event: the IP address getting resolved. You will see examples of larger event-driven callback systems in Node.js as you proceed.

Promises

Promises ease the pain of chaining callbacks. Instead of passing the callback as a parameter, you schedule it using a promise's then function. You've already seen how this is done with fetch on the client. Node.js provides an alternative DNS library that uses promises instead of callbacks, and you adapt your code to use it:

const dnsPromises = require('dns').promises;

dnsPromises.resolve(process.argv[2])
.then(matches => console.log(matches[0]))
.catch(error => console.error(error));

Note how the conditional statement of the callback is split across the then and catch channels of the promise.

Async/Await

If you really love sequential code, then this third option is for you. If you place the keyword await before a call to a function that returns a promise, then the execution will pause until the operation finishes. The value returned from the awaiting is the value that would have been passed along to the then function.

Consider this code that awaits an asynchronous result before processing it:

const result = await someAsynchronousFunction();
process(result);
// ...

This code is equivalent to this promise:

someAsynchronousFunction()
.then(result => {
process(result);
// ...
});

In a perfect world, you could rewrite your promise-based IP address utility to run sequentially like this:

const dnsPromises = require('dns').promises;

try {
const matches = await dnsPromises.resolve(process.argv[2]);
console.log(matches[0]);
} catch (error) {
console.error(error);
}

At present, however, Node.js doesn't support await at the top-level of a script. One workaround is to embed the code inside a function that has been marked as asynchronous with the async keyword:

const dnsPromises = require('dns').promises;

async function lookup() {
try {
const matches = await dnsPromises.resolve(process.argv[2]);
console.log(matches[0]);
} catch (error) {
console.error(error);
}
};

lookup();

You will see all three of these mechanisms for responding to asynchronous operations in Node.js scripts.