Asynchronous code, for all its advantages, is very difficult to analyze. It takes just a few nested callbacks with parallel operations for it to be almost impossible to figure out what's going on. It's scary to imagine a large program in which everything is asynchronous. A thousand or so lines of code and no one can understand it.
From the beginning, the developers understood the limitations of the callback approach, but it took a long time before an alternative, called Promises, appeared in JavaScript. Promises change the way the code is organized without adding new syntax. If used correctly, they allow you to “straighten out” asynchronous code and make it consistent and flat.
Most of today's JavaScript code is written using Promises, and callbacks are a thing of the past. For example, the developers of Node.js have implemented promises in almost all embedded modules. The functions that are built on promises and designed for working with file systems are available through the fs module's promises
property. Compare a few examples:
import fs from 'fs';
// The callback-based code looks kind of like a staircase
fs.readFile('./first', 'utf-8', (_error1, data1) => {
console.log(data1);
fs.readFile('./second', 'utf-8', (_error2, data2) => {
console.log(data2);
fs.readFile('./third', 'utf-8', (_error3, data3) => {
console.log(data3);
});
});
});
// The code based on promises is almost flat
// Rename promises property to fsp for brevity
const { promises: fsp } = fs;
fsp.readFile('./first', 'utf-8')
.then((data1) => console.log(data1))
.then(() => fsp.readFile('./second', 'utf-8'))
.then((data2) => console.log(data2))
.then(() => fsp.readFile('./third', 'utf-8'))
.then((data3) => console.log(data3));
Technically, a promise is a special object that keeps track of an asynchronous operation and stores its result inside itself. It's returned by all asynchronous functions built on promises.
const promise = fsp.readFile(src, 'utf-8');
It's vital to understand that a promise is not the result of an asynchronous operation. It's an object that keeps track of the operation. The operation is still asynchronous and will be executed sometime later.
const promise = fsp.readFile(src, 'utf-8');
// The file hasn't yet been read
console.log(promise);
// Promise { <pending> }
// pending is a promise state, it indicates that the operation is still in progress
How do I get the result of an asynchronous operation? There's no way to do it from the outside, it's just impossible. But you can “continue” the promise using the then()
method, within which you need to pass a callback function. The parameter of this callback will be the result of the asynchronous operation
// the result of reading the file is passed to the callback function passed to then()
// the callback will only be called when the file is read
fsp.readFile(src, 'utf-8').then((content) => console.log(content));
The callback is passed inside then()
, not called. The promise itself makes the call when the asynchronous operation is executed.
Regardless of the contents of the callback function, calling then()
always returns a new promise. And the callback function's return becomes available as a parameter of the callback of the next then()
. It's this organization of promises that allows you to build chains without having to put calls into each other, thereby avoiding Callback Hell.
// Suppose there was some text saying “Hexlet” inside the file
const promise = fsp.readFile(src, 'utf-8') // the result of the chain is ALWAYS a promise
.then((content) => `go to the next then with ${content}`) // ignore the result of the operation
.then((text) => console.log(text)); // the value from the previous then is passed into this callback, whose role is played by the log
// => go to the next then with Hexlet
// Self-check.
// What will be displayed if we add then(console.log) to the promise above?
What if the callback function returns a promise instead of just a value? Then the parameter of the next then()
will be the result of this promise. Otherwise, the promises would be meaningless. Example:
const promise = /* some kind of operation here */
// From callback comes back promise
.then(() => fsp.readFile(filePath))
// The result of the previous promise goes into the callback
.then((text) => console.log(text));
Using promises inside any function automatically makes that function asynchronous. It will no longer be possible for it to be called as a normal synchronous function because then it won't be possible to use its result, nor will it be possible to wait for the operation to complete or find errors:
// Incorrect definition
export const copy = (src, dest) => {
fsp.readFile(src, 'utf-8')
.then((content) => fsp.writeFile(dest, content));
};
// Usage
// doing something synchronous
copy(src, dest);
// do something else
As soon as there is any asynchrony in the code, the code needs to change its structure. If it's callbacks, then it gets more nested, and if it's promises, then the whole code turns into a continuous chain of promises.
// Correct Definition
export const copy = (src, dest) => {
return fsp.readFile(src, 'utf-8')
.then((content) => fsp.writeFile(dest, content));
};
// Usage
// doing something synchronous
copy(src, dest).then(() => {
// do something else
}).then(/* continue */)
.then(/* continue */);
The main advantage of promises over callbacks is that with them, asynchronous code becomes a bit like synchronous code. You can see the chain of calls, and it doesn't get deeper and deeper. At least in theory. In practice, however, promises aren't always used correctly. Look at this code:
fsp.readFile('./first', 'utf-8')
.then((data1) => {
console.log(data1);
//
// Read the file and continue the promise from this internal function
return fsp.readFile('./second', 'utf-8').then((data2) => {
console.log(data2);
// Read the file and continue the promise from this internal function
return fsp.readFile('./third', 'utf-8').then((data3) => {
console.log(data3);
});
});
});
Despite the fact that promises are used here, the code looks even more complicated than with callbacks. The problem is the way the calls are set up. The continuation of the chain does not come from the highest promise, but from each subsequent asynchronous operation. There are nested promises in theory, but only when there's no other way. In any other situation, the code should be flat and simple:
// A flat chain of promises
fsp.readFile('./first', 'utf-8')
.then((data1) => console.log(data1))
.then(() => fsp.readFile('./second', 'utf-8'))
.then((data2) => console.log(data2))
.then(() => fsp.readFile('./third', 'utf-8'))
.then((data3) => console.log(data3));
The Hexlet support team or other students will answer you.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.
Programming courses for beginners and experienced developers. Start training for free
Our graduates work in companies:
From a novice to a developer. Get a job or your money back!
Sign up or sign in
Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.