Skip to content

Latest commit

 

History

History
262 lines (203 loc) · 7.16 KB

File metadata and controls

262 lines (203 loc) · 7.16 KB

Text

In this lesson, we will learn about callbacks, promises, and async - await in JavaScript.

You already know how to read contents of a file. Because of the asynchronous nature of JavaScript, we had to pass a function which would get invoked once the file is opened and data is ready.

Let us take another example where image of a user is being displayed. To do that, we will have to:

  1. Fetch user details.
  2. Download user image.
  3. Render the image.

Let us write some JavaScript code to mock these steps. Let's create a file render.js:

// render.js
const fetchUserDetails = (userID) => {
  console.log("Fetching user details");
};

const downloadImage = (imageURL) => {
  console.log("Downloading image");
};

const render = (image) => {
  console.log("Render image");
};

Now, let's try to make it work. We will first fetch the user details for a user named john, and similar to the file reading example, we will pass a callback function which will have image URL of the user.

const fetchUserDetails = (userID, next) => {
  console.log("Fetching user details");

  setTimeout(() => {
    next(`https://image.example.com/${userID}`);
  }, 1000);
};

const downloadImage = (imageURL, next) => {
  console.log("Downloading image");
  setTimeout(() => {
    next(`Data from ${imageURL}`);
  }, 1000);
};

const render = (image) => {
  setTimeout(() => {
    console.log(`Render image: ${image}`);
  }, 1000);
};

fetchUserDetails("john", (imageURL) => {
  downloadImage(imageURL, (imageData) => {
    render(imageData);
  });
});

To run the program, execute the following command.

node render.js

You should see an out like the following.

Fetching user details
Downloading image
Render image: Data from https://image.example.com/john

Now, imagine, if you had some other functions like resizing the image, applying some transformation etc, then the sample code would look something like:

fetchUserDetails("john", (imageURL) => {
  downloadImage(imageURL, (imageData) => {
    resizeImage(imageData, (resizedImage) => {
      transformImage(resizedImage, (transformedImage) => {
        render(transformedImage);
      });
    });
  });
});

This gets complicated very quickly. This pattern is called Pyramid of doom or a callback hell.

"Pyramid of doom image"

Promises

Promises were introduced in ES6 version of JavaScript to make asynchronous code more readable. A Promise would be in one of the following states:

  • pending: initial state, neither fulfilled nor rejected.
  • fulfilled: meaning that the operation was completed successfully.
  • rejected: meaning that the operation failed.

We can create a new Promise using the following syntax.

const aPromise = new Promise((resolve, reject) => {
  //  ...
});

A promise should be either resolved or rejected to proceed further.

The following promise will be resolved after a second.

const aPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Some data");
  }, 1000);
});

The following promise will be rejected after a second.

const anotherPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("Server was unreachable!"));
  }, 1000);
});

We can either resolve the promise to change it to fulfilled state or reject it to change the state to rejected. We can chain multiple promises to mimic synchronous behaviour using then.:

aPromise
  .then(handleFulfilledA, handleRejectedA)
  .then(handleFulfilledB, handleRejectedB)
  .then(handleFulfilledC, handleRejectedC);

We can rewrite our code to use Promise.

const fetchUserDetails = (userID) => {
  console.log("Fetching user details");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`https://image.example.com/${userID}`);
    }, 1000);
  });
};

const downloadImage = (imageURL) => {
  console.log("Downloading image");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Data from ${imageURL}`);
    }, 1000);
  });
};

const render = (image) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`Render image: ${image}`);
      resolve();
    }, 1000);
  });
};

fetchUserDetails("john")
  .then((imageURL) => downloadImage(imageURL))
  .then((imageData) => render(imageData))
  .catch((err) => {
    console.error(err);
  })
  .finally(() => {
    console.log("Done!");
  });

We can further simplify the code to be:

fetchUserDetails("john")
  .then(downloadImage)
  .then(render)
  .catch((err) => {
    console.error(err);
  })
  .finally(() => {
    console.log("Done!");
  });

Any error or rejection of promise will be caught by .catch block. .finally gets executed once everything is complete on a promise chain.

Async / Await

The keywords async and await were introduced in ECMAScript 2017. It is a syntactic sugar for Promise. Even though Promise made code more readable, chaining a lot of them were still tedious.

Keyword async can only be used with a function declaration. It tells the JS runtime to wrap the function within a Promise. So a function marked as async would return a Promise and value would be returned when the promise is fulfilled.

Keyword await can only be used with a Promise. It can only be used within a function which is marked as async. The await keyword tells the JS runtime to hold the program execution till the promise is resolved or rejected.

We can now rewrite our sample code to use async / await:

const time = async (ms) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
};
const fetchUserDetails = async (userID) => {
  console.log("Fetching user details");
  await time(1000);
  return `https://image.example.com/${userID}`;
};

const downloadImage = async (imageURL) => {
  console.log("Downloading image");
  await time(1000);
  return `Data from ${imageURL}`;
};

const render = async (image) => {
  await time(1000);
  console.log(`Render image: ${image}`);
};

const run = async () => {
  try {
    const userDetails = await fetchUserDetails("john");
    const imageData = await downloadImage(userDetails);
    await render(imageData);
  } catch (err) {
    console.error(err);
  }
};
run();

We have marked functions as async and we had to write a helper funcion time to add some delay before the function returns a value.

Also since our functions are async, we had to write a run function so that we can wait on the async functions using the await keyword.

The code looks like a synchronous one and is much easier to read. Any errors that happen will get thrown and will be caught using the try..catch block.

Text

The setTimeout() method calls a function after a number of milliseconds. Syntax for setTimeout is:

setTimeout(() => {
  // callback function
}, milliseconds);

Further Reading

You can learn more about async / await from Mozilla Developer Network