Register to get access to free programming courses with interactive exercises

Promises JS: Asynchronous programming

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) => {
  fs.readFile('./second', 'utf-8', (_error2, data2) => {
    fs.readFile('./third', 'utf-8', (_error3, 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
// 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) => {
    // Read the file and continue the promise from this internal function
    return fsp.readFile('./second', 'utf-8').then((data2) => {
      // Read the file and continue the promise from this internal function
      return fsp.readFile('./third', 'utf-8').then((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.

About Hexlet learning process

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.

Get access
hours of theory

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

<span class="translation_missing" title="translation missing:">Bookmate</span>
<span class="translation_missing" title="translation missing:">Healthsamurai</span>
<span class="translation_missing" title="translation missing:">Dualboot</span>
<span class="translation_missing" title="translation missing:">Abbyy</span>
Suggested learning programs
Development of front-end components for web applications
10 months
from scratch
Start at any time

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

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.