React
Academy

Promises and Asynchronous Javascript


Sync vs. Async

A JS program is composed of a series of statements. Most statements are synchronous. That is, the next statement can't start executing until the current statement has completed.

If a synchronous statement runs for a while before it completes, it is said to block. Luckily, JavaScript has very few of those statements. (An example would be the browser alert() function.)

An asynchronous statement starts a job asynchronously. So it will not block. Instead, it will start working on the job in the background, while the main Javascript thread goes on to execute the next statements.

Eventually, when the job completes, a callback function is called with some data. A second callback function can be specified for errors. These are called success callback and failure callback.

Blocking task Async task

Why blocking matters

When JavaScript blocks, it also affects the web page displayed on your browser. The user won't be able to interact with the web page: no scrolling, no resize, no clicks nor hovers are possible on the page. (The browser will still be responsive, so the user is able to use the adress bar, the bookmarks, the back and forward buttons. Only the page itself is affected by blocking.)

If a script blocks for too long, you might get "a script is taking too long to run" dialog.

Classic callback functions

Before 2015, to consume async functions, the programmer provided the callbacks right into the function call. Here is an exemple:

function successCallback(result) {
  console.log('File is ready at URL: ' + result);
}

function failureCallback(error) {
  console.error('Error generating file: ' + error);
}

generateFile(configFile, successCallback, failureCallback);

Promise

Modern async functions now return a Promise. You attach your callbacks to the promise using then(). Lets rewrite the previous example.

//long way to call:
const promise = generateFileAsync(configFile);
promise.then(successCallback, failureCallback);

//a shorter way to write:
generateFileAsync(configFile).then(successCallback, failureCallback);

Remember

  • Callbacks are called after the success or failure of the asynchronous operation.
  • Callbacks are scheduled using the message queue.
    • They can't run until the call stack that's currently running completes.
  • Multiple callbacks may be added by calling then() several times, to chain multiple promises.

Chaining Promises

We often need multiple asynchronous operations to execute back to back. The next operation would start only once we get the result from the previous operation. We do this with a promise chain.

The then() function always returns a new promise, different from the original.

const promise = someWorkAsync();
const promise2 = promise.then(successCallback, failureCallback);

//shorter way to write
const promise2 = someWorkAsync().then(successCallback, failureCallback);

Catch

Best practice: Don't use the second argument of then to handle rejection. Instead, use the catch statement.

//avoid this way to write:
const promise = someWork()
  .then(otherWork, handleError)
  .then(finalWork, handleError);
//reject args that are undefined will be
//pushed to the next `then` down the chain

//the following is slightly better:
const promise = someWork()
  .then(otherWork)
  .then(finalWork)
  .then(null, handleError);

 //the following is the preferd way to write the code
const promise = someWork()
  .then(otherWork)
  .then(finalWork)
  .catch(handleError);

Chaining avoids the "pyramid of doom"

This is the old way of writing async methods before 2015. Notice the shape of the calls because of the chaining of inner calls.

//old way (pyramid of doom)
firstWork(function (data) {
  secondWork(
    data,
    function (data2) {
      thirdWork(
        data2,
        function (data3) {
          console.log(`Final result: ${data3}`);
        },
        failureCallback
      );
    },
    failureCallback
  );
}, failureCallback);

Here is the same code with Promises and anonymous functions

firstWork()
  .then(function (data) {
    return secondWork(data);
  })
  .then(function (data2) {
    return thirdWork(data2);
  })
  .then(function (data3) {
    console.log(`Final result: ${data3}`);
  })
  .catch(failureCallback);

It's even shorter to write using arrow functions:

firstWork()
  .then((data) => secondWork(data))
  .then((data2) => thirdWork(data2))
  .then((data3) => {
    console.log(`Final result: ${data3}`);
  })
  .catch(failureCallback);

Note: catch(failureCallback) is short for then(null, failureCallback). Remember to return results, to push the data to the next promise. Recall that return is implicit when you use an arrow function without braces.

Chaining after catch

It's possible to chain after a catch or failure. So you can execute after a failure. Here's an example:

new Promise((resolve, reject) => {
  console.log('First');
  resolve();
})
  .then(() => {
    throw new Error('Error here!');
    console.log('Second'); //not shown
  })
  .catch(() => {
    console.error('Error handled');
  })
  .then(() => {
    console.log('This shows no matter what');
  });

//Results:
//First
//Error handled
//This shows no matter what

Finally

finally will return a promise that gets settled (either fulfilled or rejected). This is great for cleanup work. (Similar to the "chaining after catch", but the finally callback does not allow parameters.)

new Promise((resolve, reject) => {
  console.log('First');
  resolve();
})
  .then(() => {
    throw new Error('Error here!');
    console.log('Second'); //not shown
  })
  .catch(() => {
    console.error('Error handled');
  })
  .finally(() => {
    console.log('Cleanup');
  });

//Results:
//First
//Error handled
//Cleanup

Promise Rejection events

Whenever a promise is rejected, one of two events is sent to the global scope (Window with browsers, Global with NodeJS, Worker in a web worker)

rejectionhandled: Sent when a promise is rejected, after it has been handled by a reject function.

unhandledrejection: Sent when a promise is rejected, when no rejection handler is available.

With both cases, the event argument will include promise (refers to the promise) and reason (the reason why it got rejected).

These offer a fallback for error handling when using promises. These handlers are global per context, so all errors will go to the same event handlers, regardless of source.

Converting synchronous code or data to a Promise

Let's take the old setTimeout syntax:

setTimeout(() => displayStuff('5 seconds passed'), 5 * 1000);

Let's convert it to a Promise wrapping the old callback by using the Promise constructor:

const wait = (seconds) => {
  return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
};
//wait is a function wrapping setTimeout
//in a new promise. Because setTimeout()
//cannot fail, we left out the reject argument
//in this case.

wait(5)
  .then(() => displayStuff('5 seconds later'))
  .catch(failureCallback);
//if displayStuff fails and throws an error,
//the failure callback will catch it.

Chaining multiple promises

If you want the promise to push some data down the chain, the function simply has to return some data. Remember that the then will return a new promise wrapping this data.

But what if you need to call a function or pass some data that isn't async, to the first element of the chain? There's no Promise yet, so you can't use then.

You can wrap any data or synced function to a Promise by using resolve. This is nice when you want to take some value or function and make it part of a Promise chain

  • Promise.resolve(data) creates an already resolved Promise. Equivalent to:
let promise = new Promise((resolve) => resolve(data));
  • Promise.reject(err) creates an already rejected Promise. (Almost never used.) Equivalent to:
let promise = new Promise((resolve, reject) => reject(error));

We can now wrap the data in a promise:

//Creates a promise that resolves the number 42
//and pushes it forward to 'data' in then
Promise.resolve(42).then((data) => {
  console.log(data);
});
// prints 42

//More Complex
function double(n) {
  return n * 2;
}

//let's chain all the promises
Promise.resolve(5)
  .then(double)
  .then(double)
  .then(double)
  .then((result) => {
    console.log(result);
  });
//prints 40  (doubled each step: 5, then 10, then 20, then 40)

//another way to chain all the promises using a reduce method
[double, double, double]
  .reduce((p, f) => p.then(f), Promise.resolve(5))
  .then((result) => {
    console.log(result);
  });
//prints 40

Helpers for chaining async functions (advanced)

Thia topic is an advanced technique, to chain async functions using helper functions. You should rarely need this. (And if you are using async-await syntax, it's unnecessary.)

We want to chain some async functions from an array, to get the following:

Promise.resolve(initValue)
  .then(func1)
  .then(func2)
  .then(func3)
  .then((result) => {
    console.log(result);
  });

Let's crete two helpers.

  • applyAsync() is an accumulator function, that chains a new promise to the existing promise:
const applyAsync = (promise, nextPromise) => promise.then(nextPromise);
  • composeAsync() function accepts a number of functions as arguments, it returns a new function that accepts an initial value to be passed through the composition pipeline.
const composeAsync = (...funcs) => (init) =>
  funcs.reduce(applyAsync, Promise.resolve(init));

Now, let's use the helpers:

const chaining = composeAsync(func1, func2, func3);
const resultPromise = chaining(initValue);
resultPromise.then((result) => {
  console.log(result);
});

//Shorter syntax:
composeAsync(
  func1,
  func2,
  func3
)(initValue).then((result) => {
  console.log(result);
});

If you are using the async/await syntax, chaining is even simpler and helpers are not necessary:

let result;
for (const f of [() => initValue, func1, func2, func3]) {
  result = await f(result);
}
console.log(result);

Start multiple functions (no chaining)

If you have a collection of Promises (an array or an iterator of Promises0, you can call them all using the following functions. Depending on how you want to handle the success or failure (resolve or reject), you will choose one method or the other.

  • Promise.all(promiseArray): Runs all functions. Returns a promise that fulfills an array with the results when all fn are resolved, or the error of the first reject one fails. (Simply: we get an array of the data if all fn resolves, OR we get the first error.)
  • Promise.allSettled(promiseArray): Runs all functions. Returns a promise that settles with an an array of objects for each fn, which resolves or rejects. (Simply: we get an array of objects containing the data or the error.)
  • Promise.race(promiseArray): Runs all functions. Returns a promise that settles with the first fn that resolves or rejects. (Simply: We get result of the first fn to settle, either the data OR an error)
  • Promise.any(promiseArray): Runs all functions. Returns a promise that settles with the first fn that resolves. If they all reject, we get an error of type AggregateError. (Simply: We get result of the first fn to resolve, OR an error if they all reject)
// all() starts the 3 functions (promises),
// returns a result array.
// The array is destructured in 3 variables
// and we print the sum of the 3 results.
Promise.all([func1, func2, func3]).then(([result1, result2, result3]) => {
  console.log(result1 + result2 + result3);
});

Async await syntax

async and await are keywords that were introdued in EcmaScript in 2017. They are syntactic sugar, simplifying the syntax to use Promises. You can convert code both ways: promises can be rewritten to use the async-await syntax, or vice-versa.

Functions marked with async always return a promise. If an async function tries to return some data, it will be wrapped in a promise.

async double(n) {
  return n*2;
}
//this will return Promise.resolve(n*2);

Async functions can use await expressions, to get data from promises. In that case, the code will suspend and yield while the promise is pending, and will resume with the rest of the code when the promise is settled.

So, await is equivalent to a then. Except now the code looks linear instead of having callbacks. (Callbacks are still created behind the curtains, by the JS engine itself.)

We can now use regular try/catch blocks, instead of .catch()

Splitting code with await

The code will be split in multiple callbacks, following an await.

async function workAsync() {
  const data = await firstWork();
  const data2 = await secondWork(data);
  const data3 = await thirdWork(data2);
  console.log(data3);
}

//is equivalent to:
function workPromise() {
  firstWork()
    .then((data) => secondWork(data))
    .then((data2) => thirdWork(data2))
    .then((data3) => {
      console.log('Final result: ' + data3);
    });
}

Using try/catch

With the async syntax, error handling is simpler: we can use regular try/catch blocks, instead of .catch():

async function workAsync(){
  try {
    const data  = await firstWork();
    const data2 = await secondWork(data);
    const data3 = await thirdWork(data2);
    console.log(data3);
  }
  catch (err) {
    errorHandling(err);
  }
}

//is equivalent to:
function workPromise(){
firstWork()
  .then((data) => secondWork(data))
  .then((data2) => thirdWork(data2))
  .then((data3) => {
    console.log('Final result: ' + data3);
  })
  .catch(errorHandling)
}

Fetch

Fetch is an API included in browsers. It uses promises to make HTTP calls.

Getting the data will take two steps:

  1. the fetch request will return a promise that resolves to a response object. With the response, you can take a look at all the headers and status. You can't access the body (yet). You'll need to check the status to check if you get an http error, like 404.
  2. you call a helper method to read the body of the response, like json(), text() or blob(). This helper will return another promise that resolves to the data you want.
//Async-await syntax
async function getData() {
  try {
    //first step
    const response = await fetch(url);
    //checks if we have an error
    if (!response.ok) {
      throw "status: " + response.status;
    }
    //second step
    const data = await response.json();
  }
  catch (err) {
    console.log(err);
  }
}

Fetch parameters

The fetch method takes two arguments. Let's take a look at the parameters.

fetch(uri, options)

fetch('https://server.com/api',
 {
  method: 'GET', // POST, PUT, DELETE, etc.
  headers: {
    'Content-Type': 'text/plain;charset=UTF-8'
    // put additional headers, here
    //(auth, bearer token, etc)
  },
  body: undefined // string, FormData, Blob
  referrer: 'about:client',
           // or "" for none,
           // or an url from the current origin
  referrerPolicy: 'no-referrer-when-downgrade',
        // no-referrer, origin, same-origin...
  mode: 'cors', // same-origin, no-cors
  credentials: 'same-origin', // omit, include
  cache: 'default', // no-store, reload...
  redirect: 'follow', // manual, error
  integrity: '', // like "sha256-1f2345a6b7890="
  keepalive: false, // true
  signal: undefined, // AbortController to abort request
})

Example of getting json using fetch

This is a basic exemple.

let url = 'https://randomuser.me/api/';

function displayUser(user) {
  console.log(`
    name:    ${user.name.first} ${user.name.last}  
    email:   ${user.email}
    country: ${user.location.country}
    picture: ${user.picture.medium}
`);
}

//Promise syntax, (same as below)
function getUserPromise(id) {
  fetch(`${url}?seed=${id}`)
    .then((response) => response.json())
    .then((data) => displayUser(data.results[0]));
}

//Async-await syntax, (same as above)
async function getUserAsync(id) {
  const response = await fetch(`${url}?seed=${id}`);
  const data = await response.json();
  displayUser(data.results[0]);
}

Fetch with error handling

This example uses error handling.

//Promise syntax, (same as below)
function getUserPromise(id) {
  fetch(`${url}?seed=${id}`)
    .then((response) => {
      if (!response.ok) {
        throw new Error('Status: ' + response.status);
      }
      return response.json();
    })
    .then((data) => displayUser(data.results[0]))
    .catch((err) => {
      console.log(err.message);
    });
}

//Async-await syntax, (same as above)
async function getUserAsync(id) {
  try {
    const response = await fetch(`${url}?seed=${id}`);
    if (!response.ok) {
      throw new Error('Status: ' + response.status);
    }
    const data = await response.json();
    displayUser(data.results[0]);
  } catch (err) {
    console.log(err.message);
  }
}

Axios

Axios is another JS library to make HTTP calls, similar to fetch(). It is also based on promises.

pros

  • You get the data in one promise call (not a two-step chained promise like fetch())
  • An error is thrown automatically if you get an http error like 404 or 500 (with fetch(), you have to check the status and throw the error).

cons

  • Axios is not part of the browser. It needs to be downloaded separately.
  • Axios is not based on standards, like fetch() is. (Fetch is part of the HTML5 standard)
//Promise syntax, (same as below)
function getUserPromise(id) {
  axios
    .get(`${url}?seed=${id}`)
    .then((response) => {
      displayUser(response.data.results[0]);
    })
    .catch(function (err) {
      console.log(err.message);
    });
}

//Async-await syntax, (same as above)
async function getUserAsync(id) {
  try {
    const response = await axios.get(`${url}?seed=${id}`);
    displayUser(response.data.results[0]);
  } catch (err) {
    console.log(err.message);
  }
}

React Academy

To get the latest version of this document, visit the handout section of ReactAcademy.live.