Categories
Blog

JavaScript Concurrency

I recently completed the excellent JavaScript Concurrency course on executeprogram.com. Here’s a quick summary of Promises and async/await in JavaScript.

What are Promises?

Promises are a feature of JavaScript that allows our code to cope with things that might not respond immediately. A classic example of this is waiting for a response from a server, where we don’t know when the data will be returned, or if it will be returned at all.

Handling Promises

Promises act as a wrapper around another value, which could be any valid JavaScript variable. To access the value contained in a Promises we can use its then() method.

then accepts a function as its single argument. For brevity this is usually passed as an arrow function, for example promise.then(v => console.log(v))

When then is executed. the value contained in the Promise is passed into the arrow function and replaced by the result of that function. In the following example we use then to multiply a Promise’s value by 2:

const p = Promise.resolve(4);
p.then( v => v * 2 );
p.then( v => console.log(v) );//4

Note that then calls always returns a promise which means they can be chained.

Catching errors

If an error is thrown inside a promise it will reject. Its status will change to rejected and the error will be passed on to the parent scope.

Promise.resolve()
  .then(() => {
    throw new Error('oops');
  });

// Result:
// Uncaught (in promise) Error: oops
// { <state>: rejected, <reason>: Error }

Promises have a method catch which can be used to intervene, allowing the promise to fulfill with a specified value rather than rejecting.

Promise.resolve()
  .then(() => {
    throw new Error('Reject!');
  })
  .catch(() => 'An error was caught');

// Result:
// { <state>: "fulfilled", <value>: 'An error was caught' }

new Promise & resolve

The new Promise() constructor accepts a function as a parameter, which is executed immediately. This function can itself accept one or two parameters which, when executed by the Promise constructor, will contain the internal resolve & reject methods of the new Promise.

const p = new Promise( 
   resolve => resolve(4)
);

p.then( v => console.log( v ) );

// Result: 4
const p = new Promise( 
   (resolve,reject) => reject('oops')
);

// Result: 
// Uncaught (in promise) oops

We can use resolve as a way to play with Promises using setTimeout:

let resolver;
const p = new Promise( resolve => resolver = resolve )
p.then( v => console.log( v ));
setTimeout( () => resolver('hey'), 1000 );
  
// Result after 1 second:
// 'hey'

Or more succinctly:

function inOneSecond( func ){
   new Promise( resolve => {
     setTimeout( () => resolve( func() ), 1000 );
   });
}

inOneSecond((()=>console.log('hey'));

// Result after 1 second: 
// 'hey'

What is async/await?

async/await is a pair of JavaScript keywords to simplify working with Promises. The async keyword is used before the function keyword when defining a function. Functions defined in this way will always return a Promise.

await is used inside an async function. It’s prepended to function calls to make the function return a Promise and execute asynchronously.

function inOneSecond( func ) => {
   new Promise( resolve => {
     setTimeout( () => resolve( func() ), 1000 );
   });
}

async function chain() {
   await inOneSecond(()=>console.log('1 second'));
   await inOneSecond(()=>console.log('2 seconds'));
}

chain();

// Result
// After 1 second: '1 second'
// After 2 seconds: '2 seconds'

Note: This example updates the inOneSecond function to return the promise it creates

async/await with arrow functions

The same example using arrow functions:

const inOneSecond = ( func ) => new Promise( 
   resolve => {
      setTimeout( () => resolve( func() ), 1000 );
   }
);

const chain = async () => {
   await inOneSecond(()=>console.log('1 second'));
   await inOneSecond(()=>console.log('2 seconds'));
}

chain();

// Result
// After 1 second: '1 second'
// After 2 seconds: '2 seconds'

Other methods of Promise

Promise.all()

  • Takes an array of promises
  • Collects their fulfilled values into an array
  • Returns a promise fulfilled with that array
  • Immediately rejects if any of the promises reject
Promise.all([
   Promise.resolve('Resolved promise 1'),
   Promise.resolve('Resolved promise 2'),
   Promise.resolve('Resolved promise 3')
]).then(
   v => console.log( v )
);

// Result: Array(3)
// 0: "Resolved promise 1"
// 1: "Resolved promise 2"
// 2: "Resolved promise 3"

Promise.allSettled()

  • Takes an array of promises
  • Collects their fulfilled or rejected values
  • Returns a promise fulfilled with an array of objects
  • Each array object has a status and a value key:
    • For fulfilled promises, the status is fulfilled
    • For rejected promises, the status is rejected
Promise.allSettled([
   Promise.resolve('Resolved promise 1'),
   Promise.reject('Rejected promise 1'),
   Promise.resolve('Resolved promise 2'),
   Promise.reject('Rejected promise 2')
]).then(
   v => console.log( v )
);

// Result: Array(4)
// 0: Object { status: "fulfilled", value: "Resolved promise 1" }
// 1: Object { status: "rejected", reason: "Rejected promise 1" }
// 2: Object { status: "fulfilled", value: "Resolved promise 2" }
// 3: Object { status: "rejected", reason: "Rejected promise 2" }

An example using the Fetch API

fetch( path )
   .then(function (response) {

      // The API call was successful!
      if (response.ok) { return response.json() }

      // There was an error
      return Promise.reject(response);

   })

   .then(function (data) {

      if ( !data.avatar ) return;

      // Add logged-in class to body
      document.body.classList.add('logged-in');

   })

   .catch(function (err) {
      // There was an error
      console.warn('Something went wrong', err);
   });