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 useful practices that make it easier to write and analyze. Take the task of merging two files once agin.

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);
    });
  });
}

Remember this code – you'll never see it looking like this again ;) Now we're going to do a little refactoring and get code that's canonical 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 smaller in size. In addition, error handling is completely gone, since promises handle errors automatically and if the code calling a function wants to catch them, it does it itself using the catch() method. But the bad news is that the code is still structured like a staircase, just like with callbacks. This code doesn't take into account the property of the promises associated with the return from 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 quite flat, this is the kind of code you should strive for when using promises. But it does present one problem. If somewhere in the chain below you need data that was received higher up, you have to drag 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 to. The main way out of this situation is to create variables through which the data can be passed on:

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 anymore, but it's still flat. The advantage of this approach becomes more and more evident as the number of promises increases. Moreover, you don't always need to pass data along.

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;
};

Although this code will work in many situations, it contains a serious error. The promise inside the promise constant isn't returned outside. The chain of promises has been broken. Any error in this promise will go unnoticed by external code. There's no guarantee that this promise will even be executed by the time the calling code expects it.

Violating the continuity (control) of asynchronous operations is a common error even among experienced programmers. In some situations, this error is immediately noticeable, in others, the code starts to behave strangely; sometimes it runs without problems, other times, it crashes with strange errors.

To prevent this from happening, you need to make sure that asynchronous operations aren't interrupted. Completing any operation must lead to some kind of reaction, 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 strictly be executed one after another. This problem can be solved using loops or folding. Whichever way you choose, the principle of building a chain won't change. A chain of promises will always look like then().then().then()....

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

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

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

// It's the function that's given in then, not its call!
const promise = filePaths.reduce((acc, path) => {
  // The accumulator is always a promise, which contains 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);

// If necessary, continue processing
promise.then((contents) => /* processing all data obtained from the files */);

Hexlet Experts

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

From a novice to a developer. Get a job or your money back!

Frontend Developer icon
Profession
beginner
Development of front-end components for web applications
start anytime 10 months

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.