Asynchronous code has many advantages, but it is extremely hard to analyze. Just a few nested callbacks with parallel operations, and it is almost impossible to understand what is going on. It's frightening to think of a large program written completely asynchronously. A thousand lines of code, and no one can understand it.
From the beginning, developers understood the limitations of the callback approach, but it took a while for an alternative to appear - promises in JavaScript.
Promises changed the way we organize code without adding new syntax. When used correctly, they allow you to straighten out asynchronous code to make it consistent and smooth.
Much of today's JavaScript code is written with promises, leaving callbacks a thing of the past. For example, Node.js developers have implemented promises in almost all embedded modules. Functions built on promises and designed to work with file systems are available via the promises
property of the fs module.
Compare some examples:
import fs from 'fs';
// The callback-based code looks 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 is essential to grasp that a promise is not the result of an asynchronous operation. It is an object that tracks the progress of the operation. The operation is still asynchronous and will be executed some time 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 to get the result of an asynchronous operation? From the outside, you can't. But you can continue the promise by using then()
method, where you pass a callback function. The parameter of this callback function 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 called inside then()
, not passed. The promise itself makes the call when the asynchronous operation is executed.
Regardless of the content of the callback function, a call to then()
always returns a new promise. The return of the callback function becomes available as a callback parameter for the next then()
.
Such organization of promises allows you to build chains without putting calls into each other, thus avoiding callback hell:
// Imagine we have a 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}`) // We ignore the result of the operation
.then((text) => console.log(text)); // The value from the previous `then` is sent to this callback, the role of which is played by the log
// => Go to the next `then` with Hexlet
What if the callback function returns a promise instead of just a value? The parameter of the next then()
would be the execution result of that promise. Otherwise, the promises would be meaningless:
const promise = /* Some kind of operation here */
// The promise is returned from the callback
.then(() => fsp.readFile(filePath))
// The result of the previous promise is placed in the callback
.then((text) => console.log(text));
Using promises within a function automatically makes that function asynchronous. We don't call it a regular synchronous function because then we can neither use its result, nor wait for the operation to complete, nor find errors:
// Incorrect definition
export const copy = (src, dest) => {
fsp.readFile(src, 'utf-8')
.then((content) => fsp.writeFile(dest, content));
};
// Usage
// Do something synchronous
copy(src, dest);
// Do something else
Once there is any asynchrony in the code, the code changes its structure. If it's callbacks, the code becomes more nested. If it's promises, the 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 */);
Incorrect usage of promises
The primary advantage of promises over callbacks is that promises make asynchronous code seem synchronous. You can see the chain of calls, and it doesn't get any deeper, at least in theory. But in practice, promises sometimes aren't 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);
});
});
});
The code looks even more complicated than it does with the callbacks, even though we are using promises here. The problem lies in the way the calls are structured. The continuation of the chain comes from each successive asynchronous operation, not from the top promise.
In theory, promises can be nested, but only when there is no other way. Otherwise, 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));
Recommended materials
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
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.