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
- Don't break the Promise chain
- Escape from callback hell
- Avoid out of scope refs
- Catch errors ASAP
- Catch unhandled errors at the end
- Do async tasks in parallel if possible
- Use finally to avoid code duplication
Doing Promises right
By sylvainpv
Doing Promises right
- 1,304