Doing JavaScript
Promises right

Promises are a pattern,
a structure invented
to make it easier to handle
a set of asynchronous tasks,

 

just like objects
are structures invented
to make it easier to handle
a set of related data

Promise chain

then() and catch() methods

return new promises
at every step

 

this is called a chainable API

 

therefore we talk about

a promise chain

 

requestA()
  .then(requestB)
  .then(requestC)
  .catch(handleErrors)
  .finally(onFinish)

// same as

const promise1 = requestA()
const promise2 = promise1.then(requestB)
const promise3 = promise2.then(requestC)
const promise4 = promise3.catch(handleErrors)
const promise5 = promise4.finally(onFinish)

1. Don't break the promise chain

// DON'T DO THIS

createUser()
  .then(function(){      
      setUserPermissions()
  })
  .then(function(){      
      addUserToGroup()
  })
  .then(function(){
      console.log("finish !")
  })
 

1. Don't break the promise chain

console.log(1)

createUser()
  .then(function(){      
      setUserPermissions().then(() => console.log(2))      
  })
  .then(function(){      
      addUserToGroup().then(() => console.log(3))      
  })
  .then(function(){
      console.log("finish !")
  })
  
 /* Logs:
 1
 finish !
 3
 2
 
 sometimes 2 is before 3, sometimes after ? The order appears to be random ?
 */
 

1. Don't break the promise chain

// IT IS THE SAME AS

requestA()
  .then(function(){
      requestB()
      return undefined // <-- SYNCHRONOUS IMMEDIATE VOID RETURN
  })
  .then(function(){
      requestC()
      return undefined // <-- SYNCHRONOUS IMMEDIATE VOID RETURN
  })
  .then(function(){
      console.log("finish !")
  })

?

B & C are lost

promise chain is broken

1. Don't break the promise chain

// DO THIS INSTEAD

requestA()
  .then(function(){
      return requestB()
  })
  .then(function(){
      return requestC()
  })
  .then(function(){
      console.log("finish !")
  })

1. Don't break the promise chain

// WHEN USING ARROW FUNCTIONS
requestA()
  .then(() => {
    // IF USING BRACKETS, DON'T FORGET THE RETURN KEYWORD
    return requestB()
  })

  // IF USING A SINGLE EXPRESSION ARROW FN, RESULT OF EXPRESSION IS RETURNED
  .then(() => requestC())

  // OR BY DIRECTLY PASSING FN BY REFERENCE
  .then(requestD)

  .then(function(){
      console.log("finish !")
  })



const square = x => x * x
const square = x => { return x * x }

2. Escape from callback hell

// DON'T DO THIS:

findUser(userName)
  .then(user => {
    return getUserPermissions(user)
      .then(permissions => {
        return checkTeamPermissions(team, permissions)
          .then(rights => {
            return rights.includes("admin")
          })
      })
  })


// DO THIS INSTEAD:
findUser(userName)
  .then(user => getUserPermissions(user))
  .then(permissions => checkTeamPermissions(team, permissions))
  .then(rights => rights.includes("admin"))

Promises have been invented precisely to avoid callback hell

3. Avoid out-of-scope refs

Scope = variable range = defines the zone from where a variable can be referred

Top level is global scope, accessible everywhere.

You should already know that global vars are a bad practice

Accessible from global scope (1):
foo

Accessible from foo scope (2):
foo, a, b, bar

Accessible from bar scope (3):
foo, a, b, bar, c

3. Avoid out-of-scope refs

// USE RETURN VALUES, AVOID VARS OUT OF SCOPE

// DON'T DO THIS:
let user, rights, permissions;

findUser(userName)
  .then(response => {
    user = response;
  })
  .then(() => {
    getUserPermissions(user)
  })
  .then(data => {
    permissions = data;
  })
  .then(() => checkUserPermissions(user, permissions))
  .then(userRights => {
    rights = userRights;
  })
  .then(() => rights.includes("admin"))


// DO THIS INSTEAD:
findUser(userName)
  // need more than what just returned the last promise ? 
  // put everything you need in a wrapper object
  .then(user => getUserPermissions(user).then(permissions => ({ user, permissions })))  
  .then(({ user, permissions }) => checkTeamPermissions(user, permissions))
  .then(rights => rights.includes("admin"))

It is hard to tell when these vars
will actually contain the data we need

Exception: component states ; dealed with additional flags like loading states...

Catching errors

There are different kind of errors:

  • the errors we know may happen and how to fix them
    "known exceptions"
  • the errors we know may happen and cannot be fixed:
    "normal errors"
  • the errors we do not yet know may happen:
    "shit we forgot"

Catching errors

findUser(userName)
  .catch(error => {
    // WILL BE EXECUTED IF THE PROMISE IS IN "REJECTED" STATE
    // WILL RETURN A NEW PROMISE IN A "RESOLVED" STATE
    
    // This is the place where we handle and try to fix the errors    
    // If you can fix the error programmatically:
    if(error.type === "ANONYMOUS_USER"){
      return new User({ name: "Anonymous" })
    }
  
    // If you cannot fix the error and want the next tasks to not execute        
    throw error; // throw the error back into the promise chain
  })
  .then(user => {
     // THIS MAY BE EXECUTED EVEN IF AN ERROR IS CATCHED BEFORE
     console.log(user); // may be "Anonymous"
     return doSomethingWithUser(user)
  })

catch() returns a promise in a resolved state !

if an error cannot be fixed,
you probably want to prevent the next then() tasks to run,

so make sure your promise chain stay in rejected state

4. Catch errors ASAP

// DON'T DO THIS:
findUser(userName)  
  .then(user => getUserPermissions(user))  
  .catch(error => {
    if(error.status === 404) showError("User does not exist")
    else if(error.status === 403) goBackToLogin()
    else if(error.type === NO_PERMISSIONS_FOUND) showError("User has no permissions")
})

// DO THIS INSTEAD:
function api(params){
  return fetch(params).catch(error => {
      if(error.status === 403){ goBackToLogin(); throw UNAUTHORIZED_ERROR; }
      throw error; // throw the error back into the promise chain
  })
}

function findUser(userName){
  return api("/user").catch(error => {
    // Handle errors ASAP. Errors are easier to fix at the lowest level
    if(error.status === 404) showError("User does not exist"); 
    throw error; // throw the error back into the promise chain
  })
}

findUser(userName)  
  .then(user => getUserPermissions(user))  
  .catch(error => {
    if(error.type === NO_PERMISSIONS_FOUND) showError("User has no permissions")
    else throw error;
  })
})

5. Catch'em all !

findUser(userName)  
  .then(user => getUserPermissions(user))  
  .catch(error => {
    if(error.type === NO_PERMISSIONS_FOUND) showError("User has no permissions")
  
    else if(KNOWN_ERRORS.includes(error)) return; // ignore errors that we manually handle before
  
    else handleUncatchedError(error); // always log unhandled exceptions somewhere
  
    // otherwise your promises will SILENTLY FAIL and you will be lost in darkness
  })
})


function handleUncatchedError(error){
  // this should not happen
  // but it may happen a lot !
  console.error("UNHANDLED EXCEPTION", error);
  // user feedback can be helpful
  showError("Something went wrong, contact admin: ", error)
  // or external monitoring in production
  logUnhandledError(error);
}

Catch all errors, even those you don't know about yet
Always catch unhandled errors at the end of a promise chain

6. Do async tasks in parallel

// DON'T DO THIS:
requestA()
  .then(responseA => {
    return requestB().then(responseB => ({ responseA, responseB }))
  })
  .then(({ responseA, responseB }) => {
    return requestC().then(responseC => ({ responseA, responseB, responseC }))
  })  
  .then(({ responseA, responseB, responseC }) => {
    // do something
  })

// DO THIS INSTEAD:
Promise.all([
  requestA(),
  requestB(),
  requestC()
]).then(([ responseA, responseB, responseC ]) => {
  // do something
})

whenever it is possible

6. Do async tasks in parallel

When it's not possible and you have to sequentially run several requests, the back-end can/should probably ease your job

Client's interests go first,

and server's job is to serve

 

So the server has to adapt to the client needs, and not the other way around

7. Finally use finally

finally() is called no matter the promise state, resolved/rejected

useful to avoid code duplication on then/catch blocks

// DON'T DO THIS:
loadResults()
  .then(response => {
    this.results = response;
    this.loading = false;
  })
  .catch(error => {    
    showError(error.message);
    this.loading = false;
  })

// DO THIS INSTEAD:
loadResults()
  .then(response => {
    this.results = response;
  })
  .catch(error => {    
    showError(error.message);
  })
  .finally(() => {
    this.loading = false;
  })

Summary

  1. Don't break the Promise chain
  2. Escape from callback hell
  3. Avoid out of scope refs
  4. Catch errors ASAP
  5. Catch unhandled errors at the end
  6. Do async tasks in parallel if possible
  7. Use finally to avoid code duplication

Doing Promises right

By sylvainpv

Doing Promises right

  • 1,304