Register to get access to free programming courses with interactive exercises

Promise chain JS: Asynchronous programming

Even with promises, it's not always clear how to structure asynchronous code. In this lesson, we'll cover some practices that make it easier to write and analyze. Again, let us consider the task of merging two files:

import fs from 'fs';

const unionFiles = (inputPath1, inputPath2, outputPath, cb) => {
  fs.readFile(inputPath1, 'utf-8', (error1, data1) => {
    if (error1) {
      cb(error1);
      return;
    }
    fs.readFile(inputPath2, 'utf-8', (error2, data2) => {
      if (error2) {
        cb(error2);
        return;
      }
      fs.writeFile(outputPath, `${data1}${data2}`, cb);
    });
  });
}

Now we'll do a refactoring and get some canonical code when working with promises. So, the first version:

import fsp from 'fs/promises';

const unionFiles = (inputPath1, inputPath2, outputPath) => {
  // Promises should always come back and be built in a chain
  const result = fsp.readFile(inputPath1, 'utf-8')
    .then((data1) => {
      const promise = fsp.readFile(inputPath2, 'utf-8')
        .then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
      return promise;
    });
  return result; // This is promise
};

The good news is that the code is clearer and shorter. Also, error handling is gone, since promises handle errors automatically, and if the code calling a function wants to catch them, it does so using the catch() method. But the bad news is that the code is still structured like a staircase, just like callbacks. This code doesn't take into account the nature of the promises associated with the return of then():

import fsp from 'fs/promises';

const unionFiles = (inputPath1, inputPath2, outputPath) => {
  const result = fsp.readFile(inputPath1, 'utf-8')
    .then((data1) => fsp.readFile(inputPath2, 'utf-8'))
    // then below is taken from the readFile promise
    .then((data2) => fsp.writeFile(outputPath, `${<how to get here data1?>}${data2}`));
  return result;
};

This version is flat, which is the kind of code to strive for when using promises. But it does present one problem. Whenever you need to retrieve data from somewhere in the chain below, you have to pass it through the whole chain. In the example above, this is the result of reading the first file.

The variable data1 is not available where the file is written. The way out of this situation is to create variables through which the data can be passed:

import fsp from 'fs/promises';

const unionFiles = (inputPath1, inputPath2, outputPath) => {
  let data1;
  return fsp.readFile(inputPath1, 'utf-8')
    .then((content) => {
      data1 = content;
    })
    .then(() => fsp.readFile(inputPath2, 'utf-8'))
    .then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
};

It's not as pretty, but flat. The advantage of this approach becomes more obvious as the number of promises increases. Also, you don't always need to pass data.

Controlling asynchronous operations

A common mistake when working with promises is losing control. Look at the slightly modified code from the examples above:

import fsp from 'fs/promises';

// What is missing here?
const unionFiles = (inputPath1, inputPath2, outputPath) => {
  const result = fsp.readFile(inputPath1, 'utf-8')
    .then((data1) => {
      const promise = fsp.readFile(inputPath2, 'utf-8')
        .then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
    });
  return result;
};

This code works in many situations, but it contains a serious bug. The promise inside the promise constant isn't returned outside. The promise chain is broken. Any error in this promise will go unnoticed by external code. There's no guarantee that this promise will execute at the time the calling code expects it.

Violating the continuity of asynchronous operations is a common error, even among experienced programmers. In some situations, this error is immediately noticeable; in others, the code behaves oddly. Sometimes it runs OK; in others, it crashes with random errors.

To prevent this, you need to make sure that asynchronous operations aren't interrupted. The completion of each operation must lead to some kind of response. There must always be something waiting for that moment.

Dynamic chain

Sometimes we don't know the number of asynchronous operations in advance, but they must execute one after the other. We can solve the problem by using loops or folding. Whichever way you choose, the principle of building a chain doesn't change. A chain of promises will always look like then().then().then()....

The only nuance you need to consider is the initial promise from which the chain starts. If such a promise doesn't exist, it can be created using the Promise.resolve() function. It will return a promise that doesn't do anything, but you can use it to start folding. Below is an example that reads a set of files sequentially and returns an array of their contents:

const filePaths = /* list of paths to files */;

// This function takes an optional value as input,
// which appears in promise.then((<here>) => ...)
// In this case, the initial value is an array
// accumulating data from files
const initPromise = Promise.resolve([]);

// It's the function that is passed in, it's not its call
const promise = filePaths.reduce((acc, path) => {
  // The accumulator is always a promise containing an array with the contents of the files
  const newAcc = acc.then((contents) =>
    // Reading the file and add its data to the accumulator
    fsp.readFile(path).then((data) => contents.concat(data)));
  return newAcc;
}, initPromise);

// Continue processing if necessary
promise.then((contents) => /* Processing all data obtained from the files */);

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
130
courses
1000
exercises
2000+
hours of theory
3200
tests

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: en.web.courses.lessons.registration.bookmate">Bookmate</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.healthsamurai">Healthsamurai</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.dualboot">Dualboot</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.abbyy">Abbyy</span>
Suggested learning programs
profession
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.