Promises

A short story with promises

const trainFolks = (folks, { min = 1, wait = 2000 }) => {
  const waitingForFolks = Promise.delay(wait).tap(() => console.log('doors closed'));
    
  return Promise.filter(folks, folk => Promise.race([
    moveIn(folk).return(folk),
    waitingForFolks.return()
  ]))
    .tap(logLength)
    .tap(rejectIfTooFew(min))
    .tap(showExample)
    .tap(explainPrinciples)
    .map(folk => doExercise(folk)
      .tap(() => Math.random() > 0.8 && askQuestion(folk).tap(() => listenAnswer(folk)))
      .return(folk)
    )
    .map(folk => standUp(folk).tap(() => moveOut(folk)).return(folk))
  ;
};

await trainFolks([1, 2, 3, 4, 5], { min: 3, wait: 3000 });

Training story

const trainFolks = async (folks, { min = 1, wait = 2000 }) => {
  const waitingForFolks = Promise.delay(wait).tap(() => console.log('doors closed'));
    
  const folks = await Promise.filter(folks, folk => Promise.race([
    moveIn(folk).return(folk),
    waitingForFolks.return()
  ]));

  logLength(folks);
  
  await rejectIfTooFew(min)(folks);
  await showExample(min)(folks);
  await explainPrinciples(folks);

  await Promise.map(folks, async folk => {
    await doExercise(folk);
    if (Math.random() > 0.8) {
      await askQuestion(folk);
      await listenAnswer(folk);
    )
  });

  await Promise.map(folks, folk => standUp(folk).tap(() => moveOut(folk)).return(folk));

  return folks;
};

await trainFolks([1, 2, 3, 4, 5], { min: 3, wait: 3000 });

Training story

From sync to async code

In synchronous code, you pull values and errors.

You work with them instantly.

In asynchronous code, values and errors are sent to you.

You tell what will happen when received.

try {
  const value = getValueSync();
  /* [...] */
catch (error) {
  /* [...] */
}
fetchValueWithCallback(function callback(error, value) => {
  /* [...] */
});

FROM SYNC TO ASYNC CODE

Implementing complex asynchronous stuff with callbacks only,

  • leads to unreadable code
  • makes harder to use pure functions

Promise to the rescue

Disclaimer: promises are not the only solution, but it is a widely adopted one.

A Promise is just a wrapper for one value or one error that you don't know yet.

const valuePromise = fetchValueAsPromise();
valuePromise
  .then(
    value => { 
      /* [...] */
    },
    error => { 
      /* [...] */
    }
  );

It still uses callbacks, but a smarter way.

A promise is a wrapper

It has helpers for building promises from anything

const valuePromise = Promise.resolve(value);
const rejectedPromise = Promise.reject(new Error('message'));

It has methods for chaining and transforming the wrapped value

const valuePromise = Promise.resolve(1);

valuePromise // a Promise wrapping 1
  .then(n => n * 2) // another Promise wrapping 2
  .then(n => n + 1) // another Promise wrapping 3
  .then(n => assert(n, 4)) // another Promise which will reject
                           // cause the assert will throw (3 !== 4)
  .catch(error => /* handle error */)
;

Promise wrapper advantages

implementations

Any-promise

  • Decide from one place which Promise implementation will be used by all your dependency using this same module

Bluebird

  • Bluebird is the most used non-native Promise implementation
  • It has adds a lot of useful helpers and methods 

Promise states

States

A Promise can only have 3 states

  • pending
  • resolved
  • rejected

Once it's resolved or rejected, it cannot change state anymore.

Same in real life actually.

Creating a Promise

With an executor

  • JavaScript Promise is a JavaScript constructor
  • a Promise is built from an executor function
  • the Promise call the executor with a resolve and a reject functions
  • the executor calls one of them
  • throwing in the executor is the same as calling reject with the error
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    const rand = Math.random();
    if (rand > 0.5) {
      resolve(rand);
    } else {
      reject(new Error('random error'));
    }
  }, 1000);
});

Chaining logic

Chaining principles

You chain with

  • p.then(s, e): s handles p resolution, e handles p rejection
  • p.catch(e): e handles p rejection (~p.then(null, e))

Basically, a promise is rejected only if the previous handler throws or rejects.

Promise.resolve(1) // p0
.then  ( s1 ) // p1
.catch ( e2 ) // p2
.then  ( s3 , e3 ) // p3
.then ( null , e4 ) // p4
.then ( s5 , e5 ) // p5

Example

  • s* and e* variables are functions
  • initially, none of them throws or rejects

Exercise: say some of the functions reject and tell which functions are eventually called and with which arguments

Chaining principles

Details: chaining

  • means telling what happens on resolution OR rejection
  • never replaces the wrapped value
  • creates another Promise wrapping the returned value
  • creates an eventually rejected Promise ONLY IF the function throws or returns an eventually rejected Promise 

Wrapping logic

Wrapping principles

  • Wrapping an eventually rejected Promise rejects with its wrapped error
  • Wrapping an eventually resolved Promise resolves with its wrapped value
  • Wrapping anything else resolves with it

Example

  • Promise.resolve(1) resolves with 1
  • Promise.resolve(Promise.resolve(2)) resolves with 2
  • Promise.resolve(Promise.reject(error)) rejects with error
     
  • Promise.resolve().then(() => 1) resolves with 1
  • Promise.resolve().then(() => Promise.resolve(2) resolves with 2
  • Promise.resolve().then(() => Promise.reject(error)) rejects with error

Basic concurrency

All

Promise.all([
  fetchUser(),
  fetchRestaurants()
])
  .then(([user, restaurant]) => {
    /* ... */
  }); 

Race

Promise.race([
  fetchUser(),
  fetchRestaurants()
])
  .then(userOrRestaurant => {
    /* ... */
  }); 

Bluebird

TODO

// @TODO

Async / Await

Thenables

// what does `f1(42)` resolve with ?
const f1 = async n => ({ then: s => s(n*2) });

// how is it similar to `f1` ?
const f2 = async n => await ({ then: s => s(n*2) });

// how does it behave the same as `f1` ?
const f3 = async n => await Promise.resolve(n*2);

// how does it behave the same as `f1` ?
const f4 = async n => Promise.resolve(n*2);
// why does it fail ?
const fa = async n => await ({ then: async s => s(await n*2) });

// why does it fail ?
const fb = async n => await ({ then: s => s(await Promise.resolve(n*2)) });

// why does it succeed ?
const fc = async n => await ({ then: async s => s(await Promise.resolve(n*2)) });
// who arrives first in both cases?
process.nextTick(() => console.log(0)); f2(42).then(console.log);
process.nextTick(() => console.log(0)); f3(42).then(console.log);

// now you know what "await" does with "thenables"

DEFINITION

typescript short Definition

declare class P<T, E> {

  constructor(executor: (
    resolve: (result: T) => void,
    reject: (error: any) => void
  ) => void);

  static resolve<T, E>  (result: T | P<T, E>):    P<T   , E>;
  static reject<E>      (error: E):               P<void, E>;
  static all<T, E>      (args: (T | P<T, E>)[]):  P<T[] , E>;
  static race<T, E>     (args: (T | P<T, E>)[]):  P<T   , E>;

  then<U, F>(
    transform?:      (result: T) => U | P<U, F>,
    transformError?: (error:  E) => U | P<U, F>
  ): P<U, F>;

  catch<U, F>(
    transformError?: (error:  E) => U | P<U, F>
  ): P<U, F>;

}

Here we write P instead of Promise for convenience only

declare class P<T, E> {

  constructor(executor: (
    resolve: (result: T) => void,
    reject: (error: any) => void
  ) => void);

  static resolve<T, E>  (result: W<T, E>):  P<T   , E>;
  static reject<E>      (error: E):         P<void, E>;
  static all<T, E>      (args: W<T, E>[]):  P<T[] , E>;
  static race<T, E>     (args: W<T, E>[]):  P<T   , E>;

  then<U, F>(
    transform?:      (result: T) => W<U, F>,
    transformError?: (error:  E) => W<U, F>
  ): P<U, F>;

  catch<U, F>(
    transformError?: (error:  E) => W<U, F>
  ): P<U, F>;

}

declare type W<T, E> = T | P<T, E>;

typescript short Definition

We just extracted the type W<T,E> = either T, or a Promise eventually resolved with T or rejected with E

If you have a doubt, you can report to the strict behavior definitions of promises: https://promisesaplus.com 

Exercises

  • Promise.all
  • queueing
  • using Bluebird
    • map
    • props
    • series
    • tap/tapCatch
    • spread
    • get

?

Promises

By Alexis Tondelier

Promises

  • 634