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
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)
// DON'T DO THIS
createUser()
.then(function(){
setUserPermissions()
})
.then(function(){
addUserToGroup()
})
.then(function(){
console.log("finish !")
})
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 ?
*/
// 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
// DO THIS INSTEAD
requestA()
.then(function(){
return requestB()
})
.then(function(){
return requestC()
})
.then(function(){
console.log("finish !")
})
// 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 }
// 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
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
// 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...
There are different kind of 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
// 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;
})
})
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
// 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
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
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;
})