DEV Community

loading...

9 Promising Promise Tips

kepta profile image Kushan Joshi ใƒปUpdated on ใƒป8 min read

Promises are great to work with! Or so does your fellow developer at work says.

prom

This article would give you to the point no bullshit tips on how to improve your relationship with the Promises.

1. You can return a Promise inside a .then

Let me make the most important tip standout

Yes! you can return a Promise inside a .then

Also, the returned promise is automatically unwrapped in the next .then

.then(r => {
    return serverStatusPromise(r); // this is a promise of { statusCode: 200 }
})
.then(resp => {
    console.log(resp.statusCode); // 200; notice the automatic unwrapping of promise
})
Enter fullscreen mode Exit fullscreen mode

2. You create a new Promise everytime you do .then

If you are familiar with the dot chaining style of javascript you would feel at home. But for a newcomer this might not be obvious.

In promises whenever you .then or .catch you are creating a new Promise. This promise is a composition of the promise you just chained and the .then / .catch you just attached.

Let us look at an example:

var statusProm = fetchServerStatus();

var promA = statusProm.then(r => (r.statusCode === 200 ? "good" : "bad"));

var promB = promA.then(r => (r === "good" ? "ALL OK" : "NOTOK"));

var promC = statusProm.then(r => fetchThisAnotherThing());
Enter fullscreen mode Exit fullscreen mode

The relationship of above promises can be described neatly in a flow chart:
image

The important thing to note here is that promA, promB and promC are all different promises but related.

I like to think of .thening as a big massive plumbing where water will stop flowing to the children when the parent node malfunctions. For eg. if promB fails, no other node will be affected but if statusProm fails all the nodes will be affected i.e. rejected.

3. A Promise is resolved/rejected for EVERYONE

I find this as one of the most important thing that makes promises great to work with. To put it simply, if a promise is shared between multiple parts of your app, all of them would get notified when it gets resolved/rejected.

This also means nobody can ever mutate your promise, so please feel free to pass it around without worrying.

function yourFunc() {
  const yourAwesomeProm = makeMeProm();

  yourEvilUncle(yourAwesomeProm); // rest assured you promise will work, regardless of how evil uncle consumes your promise

  return yourAwesomeProm.then(r => importantProcessing(r));
}

function yourEvilUncle(prom) {
  return prom.then(r => Promise.reject("destroy!!")); // your evil uncle
}
Enter fullscreen mode Exit fullscreen mode

In the example above you can see that promise by design makes it difficult for anyone to do nefarious things. As I said above, Keep calm and pass the promise around

4. Promise Constructor is not the solution

I have seen fellow developers exploiting the constructor style everywhere, thinking they are doing it the promise way. But this is a big lie, the actual reason is that the constructor API is very similar to the good old callback API and old habits die hard.

If you find yourself writing Promise constructors everywhere, you are doing it wrong!

To actually take a step forward and move away from callbacks, you need to carefully minimize the amount of Promise constructor's you use.

Let us jump to the actual use case of a Promise constructor:

return new Promise((res, rej) => {
  fs.readFile("/etc/passwd", function(err, data) {
    if (err) return rej(err);
    return res(data);
  });
});
Enter fullscreen mode Exit fullscreen mode

Promise constructor should only be used when you want to convert a callback to promise.
Once you have grasped this beautiful way of creating promises, it can become really tempting to use it at other places which are already promisified!

Let us look at a redundant Promise constructor

โ˜ ๏ธWrong

return new Promise((res, rej) => {
    var fetchPromise = fetchSomeData(.....);
    fetchPromise
        .then(data => {
            res(data); // wrong!!!
        })
        .catch(err => rej(err))
})
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’–Correct

return fetchSomeData(...); // when it looks right, it is right!
Enter fullscreen mode Exit fullscreen mode

Wrapping a promise with Promise constructor is just redundant and defeats the purpose of the promise itself.

๐Ÿ˜ŽProtip

If you are a nodejs person, I recommend checking out util.promisify. This tiny thing helps you convert your node style callback into promises.

const {promisify} = require('util');
const fs = require('fs');

const readFileAsync = promisify(fs.readFile);

readFileAsync('myfile.txt', 'utf-8')
  .then(r => console.log(r))
  .catch(e => console.error(e));
Enter fullscreen mode Exit fullscreen mode

5. Use Promise.resolve

Javascript provides Promise.resolve, which is a short hard for writting something like this:

var similarProm = new Promise(res => res(5));
// ^^ is equivalent to
var prom = Promise.resolve(5);
Enter fullscreen mode Exit fullscreen mode

This has multiple use cases and my favourite one being able to convert a regular (sync) javascript object into a promise.

// converting a sync function to an async function
function foo() {
  return Promise.resolve(5);
}
Enter fullscreen mode Exit fullscreen mode

You can also use it as a safety wrapper around a value which you are unsure whether it is a promise or regular value.

function goodProm(maybePromise) {
  return Promise.resolve(maybePromise);
}

goodProm(5).then(console.log); // 5

goodProm(Promise.resolve(Promise.resolve(5))).then(console.log); // 5, Notice it unwraps all the layers of promises automagically!
Enter fullscreen mode Exit fullscreen mode

6.Use Promise.reject

Javascript also provides Promise.reject, which is a short hand for this

var rejProm = new Promise((res, reject) => reject(5));

rejProm.catch(e => console.log(e)) // 5
Enter fullscreen mode Exit fullscreen mode

One of my favourite use case is rejecting early with Promise.reject.

function foo(myVal) {
    if (!mVal) {
        return Promise.reject(new Error('myVal is required'))
    }
    return new Promise((res, rej) => {
        // your big callback to promie conversion!
    })
}
Enter fullscreen mode Exit fullscreen mode

In simple words, use Promise.reject wherever you want to reject promise.

In the example below I use inside a .then

.then(val => {
  if (val != 5) {
    return Promise.reject('Not Good');
  }
})
.catch(e => console.log(e)) // Not Good
Enter fullscreen mode Exit fullscreen mode

Note: You can put any value inside Promise.reject just like Promise.resolve. The reason you often find Error in a rejected promise is that it is primarily used for throwing an async error.

7. Use Promise.all

Javascript provides Promise.all, which is a shorthand for โ€ฆ. well I can't come up with this ๐Ÿ˜.

In a pseudo algorithm, Promise.all can be summarised as

Takes an array of promises

    then waits for all of them to finish

    then returns a new Promise which resolves into an Array

    catches if even a single fails/rejects.
Enter fullscreen mode Exit fullscreen mode

The following example shows when all the promises resolve:

var prom1 = Promise.resolve(5);
var prom2 = fetchServerStatus(); // returns a promise of {statusCode: 200}

Proimise.all([prom1, prom2])
.then([val1, val2] => { // notice that it resolves into an Array
    console.log(val1); // 5
    console.log(val2.statusCode); // 200
})
Enter fullscreen mode Exit fullscreen mode

This one shows when one of them fails:

var prom1 = Promise.reject(5);
var prom2 = fetchServerStatus(); // returns a promise of {statusCode: 200}

Proimise.all([prom1, prom2])
.then([val1, val2] => {
    console.log(val1); 
    console.log(val2.statusCode); 
})
.catch(e =>  console.log(e)) // 5, jumps directly to .catch
Enter fullscreen mode Exit fullscreen mode

Note: Promise.all is smart! In case of a rejection, it doesn't wait for all of the promises to complete!. Whenever any promise rejects, it immediately aborts without waiting for other promises to complete.

๐Ÿ˜ŽProtip
Promise.all does not provide a way to execute promises in batches(concurrency), since by design promises are executed the moment they are created. If you want to control the execution, I recommend trying out Bluebird.map. (Thanks Mauro for this tip.)

8. Do not fear the rejection OR

Do not append redundant .catch after every .then

How often do we fear errors being gobbled up somewhere in between?

To overcome this fear, here's a very simple tip:

Make the rejection handling the problem of the parent function.

Ideally, rejection handling should be at the root of your app and all the promise rejections trickle down to it.

Do not fear writing something like this

return fetchSomeData(...);
Enter fullscreen mode Exit fullscreen mode

Now if you do want to handle the rejection in your function, decide whether you want to resolve things or continue the rejection.

๐Ÿ’˜ Resolving a rejection

Resolving rejection is simple, in theย .catch whatever you return would be assumed to be resolved. However there is a catch (pun intended), if you return a Promise.reject in aย .catch the promise will be rejected.

.then(() => 5.length) // <-- something wrong happenned here
.catch(e => {
        return 5;  // <-- making javascript great again
})
.then(r => {
    console.log(r); // 5
})
.catch(e => {
    console.error(e); // this function will never be called :)
})
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’”Rejecting a Rejection
To reject a rejection is simple, don't do anything. As I said above, let it be some other functions problem. More often than not, parent functions have a better way to handle the rejection than your current function.

The important thing to remember is, once you write a catch it means you are handling the error. This is similar to how sync try/catch works.

If you do want to intercept a rejection: (I highly recommend not!)

.then(() => 5.length) // <-- something wrong happenned here
.catch(e => {
  errorLogger(e); // do something impure
  return Promise.reject(e); // reject it, Yes you can do that!
})
.then(r => {
    console.log(r); // this .then (or any subsequent .then) will never be called as we rejected it above :)
})
.catch(e => {
    console.error(e); //<-- it becomes this catch's problem
})
Enter fullscreen mode Exit fullscreen mode

The fine line between .then(x,y) and then(x).catch(x)
The .then accepts a second callback parameter which can also be used to handle errors. This might look similar to doing something like then(x).catch(x), but both these error handlers differ in which error they catch.

I will let the following example speak for itself.

.then(function() {
   return Promise.reject(new Error('something wrong happened'));
}).catch(function(e) {
   console.error(e); // something wrong happened
});


.then(function() {
   return Promise.reject(new Error('something wrong happened'));
}, function(e) { // callback handles error coming from the chain above the current `.then`
    console.error(e); // no error logged
});
Enter fullscreen mode Exit fullscreen mode

The .then(x,y) comes really handy when you want to handle an error coming from the promise you are .thening and not want to handle from the .then you just appended to the promise chain.

Note: 99.9% of the times you are better off using the simpler then(x).catch(x) .

9. Avoid the .then hell

This tip is pretty simple, try to avoid the .then inside a .then or .catch. Trust me it can be avoided more often than you think.

โ˜ ๏ธWrong

request(opts)
.catch(err => {
  if (err.statusCode === 400) {
    return request(opts)
           .then(r => r.text())
           .catch(err2 => console.error(err2))
  }
})
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’–Correct

request(opts)
.catch(err => {
  if (err.statusCode === 400) {
    return request(opts);
  }
  return Promise.reject(err);
})
.then(r => r.text())
.catch(err => console.erro(err));
Enter fullscreen mode Exit fullscreen mode

Sometimes it does happen that we need multiple variables in a .then scope and there is no option but to create another .then chain.

.then(myVal => {
    const promA = foo(myVal);
    const promB = anotherPromMake(myVal);
    return promA
          .then(valA => {
              return promB.then(valB => hungryFunc(valA, valB)); // very hungry!
          })
})
Enter fullscreen mode Exit fullscreen mode

I recommend using the ES6 destructuring power mixed with Promise.all to the rescue!

.then(myVal => {
    const promA = foo(myVal);
    const promB = anotherPromMake(myVal);
    return Promise.all([prom, anotherProm])
})
.then(([valA, valB]) => {   // putting ES6 destructing to good use
    console.log(valA, valB) // all the resolved values
    return hungryFunc(valA, valB)
})
Enter fullscreen mode Exit fullscreen mode

Note: You can also use async/await to solve this problem if your node/browser/boss/conscious allows!

I really hope this article helped you in understanding Promises.

Please check out my previous blog posts.

If you โค๏ธ this article, please share this article to spread the words.

Reach out to me on Twitter @kushan2020.

Discussion (17)

pic
Editor guide
Collapse
jymbob profile image
jymbob

Nice. I may be in the 0.01% that needs to use .then(x, y) so thanks for explaining the difference

Also in your first example, I'm pretty sure

.then(r => {
    return serverStatusPromise(r); // this is a promise of { statusCode: 200 }
})
.then(

...can be simplified to

.then(serverStatusPromise)
.then(

... because of function currying, assuming ServerStatusPromise accepts one value and returns one value.

This means you can do things like:

fetch('https://example.com/')
.then(validateResponse)
.then(readResponseAsJSON)
.catch(handleResponseError)
Collapse
kepta profile image
Kushan Joshi Author • Edited

I agree that that code can be simplified into the one you mentioned, but in my humble opinion:

  • I personally find it hard to follow what is going on when you simply pass a callback reference.
  • Using this style would make it difficult for new comers to actually understand what is going on.
  • It works fine for unary callbacks like in promises, but it is a source of cryptic bugs when there is an arity mismatch between the caller and the callback.
  • It also becomes a problem when you want pass method which is context (this) sensitive. I have seen folks doing .catch(console.error.bind(console)) just to avoid that, when they could simply have used a .catch((err)=> console.error(err)).
Collapse
mgtitimoli profile image
Mauro Gabriel Titimoli • Edited

Hi Kushan,

Thanks for having taken the time to write this, great post.

Just a couple of things that they are not correct:

  1. When you wrote the pseudo code to describe Promise.all, you wrote: then runs all of them simultaneously; which it's not true, as promises given to Promise.all have already started
  2. The "correct" example you wrote for (9), is not entirely OK, it should be like follows:
request(opts)
.catch(err =>
  err.statusCode === 400
    ? request(opts)
    : Promise.reject(err)) // you missed rejecting in your example
.then(r => r.text())
.catch(err => console.error(err));
Collapse
kepta profile image
Kushan Joshi Author • Edited

Hey Mauro,
Thanks for giving the feedback.

  1. Yes I agree, I think I tried to convey that it doesn't do any sort of batch execution, but then promises by design run the moment they are created. I think I should have an additional tip which clarifies about how promises run.

  2. Whoops, missed that, corrected it :)

Collapse
brunoscopelliti profile image
Bruno Scopelliti

Hi, since we're talking of Promise, these days I'm publishing a series of videos about how to build a promise polyfill, with the stated goal of making clear how promise works under the hood.
If this sounds as something interesting, here's the first video: youtube.com/watch?v=E_p-PVNqhZE

Collapse
annarankin profile image
Anna Rankin

Thanks so much for this! I never knew about Promise.reject/Promise.resolve. Awesome :D

Collapse
danielfrugoni profile image
DanielFrugoni

Hi, hope you can help me.
having this:
async searchDatoReg(locclave: string, locmail: string) {
this.mensaje = null;

return new Promise((resolve) => {
this.api.searchProductoLg(locclave, locmail).subscribe(
(data) => {
if (isUndefined(data[0])) {
this.op = 0;
this.filtrados = null;
console.log('tempx1:');
} else {
this.filtrados = { ...data };
this.op = 1;
console.log('tempx2:');

      }
    }
  )
}
)

}
then:
await this.searchDatoReg(locclave, locmail);

   if (this.op === 0){
      console.log('aca 1')
      console.log(JSON.stringify(this.filtrados));
      this.glo.asignaReg(this.filtrados[0].email,this.filtrados[0].clave,this.filtrados[0].nombre,this.filtrados[0].id);
    } else {
      console.log('aca 2');
      console.log(JSON.stringify(this.filtrados));
      this.glo.asignaReg(locmail,locclave,'',0);
    }

dosent syncronize the call, should first do this.searchDatoReg(..) then the rest
but never does it
any hint please

Collapse
westc profile image
Chris West

Thanks so much for the post. I always love learning more about JavaScript. In fact, I recently created a topsites site that I think this link belongs on. It is at ciphly.com?languages=JavaScript. Please consider adding this post and other informative ones like it!

Collapse
nimatrazmjo profile image
Nimatullah Razmjo • Edited

Hi Kushan,

Thanks for explaining the promise in a very good way especially Promise constructor.

I have a question.

In promise all. I have five promise which runs concurrently. If promise one resolve and promise two got a problem will go directly to catch part of Promise.all. does it rollback the promise one changes or not.

I am a bit confused.

Or let's say. In Promise.all, if an error occurs to single promise. does it rollback all other promises changes?

Thanks in advance.

Collapse
kepta profile image
Kushan Joshi Author • Edited

I am not clear what you mean by rolling back.

Let us look at a modified point 7's example:


const prom1 =  fetchComments().then(comments => console.log(comments));
const prom2 = fetchPosts().then(posts => console.log(posts))


Promise.all([prom1, prom2])
.then(() => {
  console.log('Hello');
})

If I understand your doubt correctly, imagine the above situation in which there are 2 promises prom1 and prom2. Now if prom2 fails because fetchPosts fails, the following things will happen:

  • console.log(comments) would be shown to users, since promises are immutable and prom1 has nothing to do with prom2 failing.
  • console.log(posts) would NOT be shown to users, since prom2 has failed.
  • console.log('Hello') would NOT be shown to users, since it is a new promise composed of prom1 & prom2, in which prom2 has failed.

To understand Promise.all correctly. You need to understand that Promise.all creates a new promise whose outcome is dependant on the promises (eg. prom1 and prom2) it was composed with.

Let us use our previous example and this time name the new promise:

const resultPromise = Promise.all([prom1, prom2]);

// This is exactly same as the previous example, just with improved readability with the help of variable naming
resultPromise.then(() => {
  console.log('Hello');
})

The outcome of resultPromise is:

  • resolved: If all of the promises resolve.
  • rejected: If one or more promises rejects. In our example prom2 rejected and hence resultPromise would also be rejected.

Now you can clearly see the dependency of all these promises and their outcome. (btw reread the point 2, I hope this comment will make it easier).

Collapse
yujinyuz profile image
Yujin

thanks for this post! Gave me some idea how to understand Promises on open-source projects :D

Collapse
brenonaraujo profile image
Brenon Araujo

Hi kushan,

Thanks for sharing, this post helps me a lot.

Collapse
thibmaek profile image
Thibault Maekelbergh

Best promise article ever

Collapse
happy32x profile image
happy julien

Thank you very much... You great

Collapse
rkristelijn profile image
Remi Kristelijn

Very good read! Finally a simple piece of text that explains how to do stuff and not how to not do stuff.

Collapse
amitphulera profile image
Amit Phulera

That is a very informative post on Promises.
Thanks for writing :)

Collapse
chardos profile image
Richard Tan

Learnt a lot. Thanks Kushan!