loading...
Cover image for I promise I won't callback anymore

I promise I won't callback anymore

damcosset profile image Damien Cosset Originally published at damiencosset.com ・5 min read

Introduction

Dealing with the asynchronous nature of Javascript can be very challenging and frustrating. Callbacks have been for a long time the default way to do things. ES6 gave us a alternative to callbacks with promises. Promises are natively available in Node.js since version 4.

What is that?

A promise is an abstraction that allows a function to return an object called promise. A promise is the eventual result of an asynchronous operation. We say that a promise is pending when the asynchronous operation is not complete. A promise is fulfilled when the operation has been successfully completed. A promise is rejected when the operation failed.

Constructing a promise

In ES6, you can create a promise with the Promise constructor. It takes a function with two parameters, usually called resolve and reject. resolve is the function we will call when our promise is fulfilled, reject will be called when our promise is rejected.

Let's start with a function that returns a promise. This promise will always be fulfilled.

const myPromise = () => {
  return new Promise( ( resolve, reject ) => {
  console.log('I promise!')
  resolve()
})
}

myPromise()
.then(() => {
  console.log('I made it!')
})

// I promise!
// I made it!

myPromise returns a promise. When we call our function, the promise is pending, it's neither fulfilled or rejected. We print out I promise! and we call the resolve function. The then() method is responsible for handling a fulfilled promise. The resolve() call triggers the then() method and we print out I made it!

Let's now see a rejected promise:


const rejectedPromise = () => {
  return new Promise( ( resolve, reject ) => {
    console.log('I promise!')
    reject('You lied to me!!')
  })
}

rejectedPromise()
.then(() => {
  console.log('I made it!')
})
.catch(err => {
  console.log('How dare you?')
  console.log(err)
})

// I promise!
// How dare you?
// You lied to me!!

Here, our promise calls the reject function, meaning that our promise is rejected. This triggers the catch method. It is a good practice to call reject with an error message. Note that the then() method is NOT called in this case.

I promise then I promise then I promise then I promise then ...

The amazing thing about promises is the ability to chain them. If we take our previous example and add an extra then():

rejectedPromise()
.then(() => {
  console.log('I made it!')
})
.catch(err => {
  console.log('How dare you?')
  console.log(err)
})
.then(() => {
  console.log('I forgive you no matter what.')
})

//I promise!
//How dare you?
//You lied to me!!
//I forgive you no matter what.

This last then() will always run. If our promise is fulfilled, the first then will be executed, the catch will be skipped, and finally our last then will be run.

Let's create three promises and chain them:


const promiseToLove = iAmMature => {
  return new Promise( ( resolve, reject ) => {
    if( iAmMature ){
      resolve('I love you so much!')
    } else {
      reject("It's not you, it's me...")
    }
  })
}

const promiseToProtect = iAmNice => {
  return new Promise( ( resolve, reject ) => {
    if( iAmNice ){
      resolve('I promise I will protect you!')
    } else {
      reject('What? Get lost!')
    }
  })
}

const promiseToBeHereOnTime = hairLooksGood => {
  return new Promise( ( resolve, reject ) => {
    if( hairLooksGood ){
      resolve('I promise I will be there!')
    } else {
      reject('How about tomorrow?')
    }
  })
}

//First promise
promiseToLove(true)
.then(statement => {
  console.log(statement)
})
.catch(statement => {
  console.log(statement)
})
//returns another promise
.then(() => promiseToProtect(true))
//handles our second promise
.then(statement => {
  console.log(statement)
})
.catch(statement => {
  console.log(statement)
})
// returns annother promise
.then(() => promiseToBeHereOnTime(true))
// handles our third promise
.then(statement => {
  console.log(statement)
})
.catch(statement => {
  console.log(statement)
})
// this always runs
.then(() => {
  console.log('And they lived happily ever after!!!')
})

// I love you so much!
// I promise I will protect you!
// I promise I will be there!
// And they lived happily ever after!!!

Our three functions take a single parameter ( a boolean ). If the argument is set to true, the promise will be fulfilled, otherwise, it will be rejected. Once a promise is settled, we return another one and deal with that one...

Can you see how much more elegant promises make dealing with the asynchronous nature of Javascript? No need to nest an infinite amount of callbacks. It's clean, it's beautiful. I'll let you imagine how the code would look like if we had callbacks here instead of promises.

Just for fun, let's set everything to false, because some people can't keep their promises...

//First promise
promiseToLove(false)
.then(statement => {
  console.log(statement)
})
.catch(statement => {
  console.log(statement)
})
//returns another promise
.then(() => promiseToProtect(false))
//handles our second promise
.then(statement => {
  console.log(statement)
})
.catch(statement => {
  console.log(statement)
})
// returns annother promise
.then(() => promiseToBeHereOnTime(false))
// handles our third promise
.then(statement => {
  console.log(statement)
})
.catch(statement => {
  console.log(statement)
})
// this always runs
.then(() => {
  console.log('Why are you like this?')
})

// It's not you, it's me...
// What? Get lost!
// How about tomorrow?
// Why are you like this?

Promises in real life

In Node.js, not all functions support promises out of the box. To solve this, you can use the promisify method in the util module. It takes a function and transforms it into a function that returns a promise.

Cloning a file

To clone a file, we will read its content then write it to a new file. Callback style, you would have something like this:

const fs = require('fs')

fs.readFile('myFile.js', 'utf-8', (err, data) => {
  fs.writeFile('clone.js', data, err => {
    if(err){
      throw err
    } else {
      console.log('All done')
    }
  })
})

Ok, we can already see the gates of callback hell in the distance. Let's promisify this thing. I will even write a file first, then read it, then write in a new one, then read our new clone. Yeah, I know, I'm nuts...

const fs = require('fs')

// Get the promisify method from the util module
const { promisify } = require('util')

// Promisify our readFile and writeFile function
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)

writeFile('original.txt', 'Promise me you will clone me!')
.then(() => readFile('original.txt', 'utf-8'))
.then(content => writeFile('clone.txt', content))
.then(() => readFile('clone.txt', 'utf-8'))
.then(cloneContent => console.log(cloneContent))
.catch(err => console.log('Error occured:', err))


// Promise me you will clone me!

Yeah, that's sexy. Why would you write with callbacks anymore? Our writeFile and readFile return either the file's content when their resolve() is called, or the error message if their reject() is called. In our example, I only wrote one catch(). But this catch() will be called if any of the promises before is rejected:

writeFile('original.txt', 'Promise me you will clone me!')
.then(() => readFile('404NOTFOUND.txt', 'utf-8')) // <= Error here
.then(content => writeFile('clone.txt', content))
.then(() => readFile('clone.txt', 'utf-8'))
.then(cloneContent => console.log(cloneContent))
.catch(err => console.log('Error occured:', err)) // <= Trigger this


//Error occured: { Error: ENOENT: no such file or directory, open //'404NOTFOUND.txt'
//  errno: -2,
//  code: 'ENOENT',
//  syscall: 'open',
//  path: '404NOTFOUND.txt' }

Alright, this should be more than enough to get you started with your own promises. Save your sanity, make your code cleaner, use promises and not callbacks :)

Discussion

pic
Editor guide
Collapse
ben profile image
Ben Halpern

This really gets to the bottom of the benefit. I used to really hate writing async JS, and now it's really quite pleasant.

Collapse
mrm8488 profile image
Collapse
developius profile image
Finnian Anderson

Promise.all is super useful when you've got a ton of promises to execute, although I intensely dislike how it returns an array of the results - it would be nice to have some way of naming them before passing them in (object?) because otherwise you end up just assigning them all, which I find ugly:

Promise
  .all([ p1, p2, p3 ])
  .catch(console.log)
  .then(data => {
    let user_data = data[1]
    let stats = data[2]
    let other_info = data[3]
})

This would be nicer:

Promise
  .all({ user: p1, stats: p2, info: p3 })
  .catch(console.log)
  .then(console.log)

/*
{
  "user": user_data,
  "stats": stats_data,
  "info": info_data
}
*/

Collapse
damcosset profile image
Damien Cosset Author

I haven't used Promise.all very often. I think I agree with the returned array being weird. You can pass an array of objects:

Promise.all([{time: promiseToBeHereOnTime(true)}, {love: promiseToLove( true )}, {protect: promiseToProtect( true )}] )
.then(content => {
  console.log(content)
  console.log('Done! All fulfilled')
})
.catch(err => {
  console.log(err)
})

/*
[ { time: Promise { 'I promise I will be there!' } },
  { love: Promise { 'I love you so much!' } },
  { protect: Promise { 'I promise I will protect you!' } } ]
Done! All fulfilled
*/

I don't know, it probably would be easier to tweak the Promise.all implementation to server a specific purpose.

Collapse
developius profile image
Finnian Anderson

You still have the problem with an array being returned though - it's ugly. Named properties would be much nicer. The specific purpose would be to avoid this:

Promise
  .all(proms)
  .then(data => {
    let a = data[0]
    let b = data[1]
    let c = data[1]
    doSomethingWith(a)
    doSomethingWith(b)
    doSomethingWith(c)
  })
  .catch(console.log)

And replace it with this:

Promise
  .all(proms)
  .then(data => {
    doSomethingWith(data.a)
    doSomethingWith(data.b)
    doSomethingWith(data.c)
  })
  .catch(console.log)
Collapse
developius profile image
Finnian Anderson

Just spotted this, which gives another interesting approach. Still too much code duplication for my liking though. dev.to/mrm8488/using-es6-array-des...

Collapse
kspeakman profile image
Kasey Speakman

The normal behavior like you demonstrate with the last examples is nice. I do find really confusing the behavior of a then after a catch. It behaves like a "finally", but that's somehow unexpected to me.

Collapse
damcosset profile image
Damien Cosset Author

Agreed, it is a bit confusing. A lot of librairies/packages implement a finally method, hopefully we will see it soon. I wonder if they are thinking about adding it in the future spec

Collapse
robihood23 profile image
Robert Gogolan

@Finnian Anderson

I guess you can try to use destructuring in the handler

Promise
  .all([ p1, p2, p3 ])
  .catch(console.log)
  .then(([user_data, stats, other_info] = data) => {
     doSomethingWith(user_data)
     doSomethingWith(stats)
     doSomethingWith(other_info)
})

or directly

Promise
   .all([p1, p2, p3])
   .then(([p1result, p2result, p3result]) => {
    console.log(p1result, p2result, p3result);
   })
Collapse
math2001 profile image
Mathieu PATUREL

const { promisify } = require('util')

So neat!! Didn't know about this function. Also note that if you want to have this already done, you can use mz (I was gonna say fs-promise, but it's been deprecated...).

Next step: async and await!

Collapse
damcosset profile image
Damien Cosset Author

Oh, I didn't know about mz package, good to know.

Yep, now I have to explore async/await. Seems like the new logical wait to deal with asynchronous stuff in Javascript

Collapse
nickytonline profile image
Nick Taylor (he/him)

Nice write up! I think it's really great that they added util.promisify. You should do a follow up to this post on async/await. 🔥

Collapse
bengreenberg profile image
Ben Greenberg

This is great, thanks!

Collapse
mrm8488 profile image
Manuel Romero

Using E6 Array destructors is so handful for Promise.all -> medium.com/@3f2bb9b4510b/7a952a8581c3

Collapse
adam_cyclones profile image
Adam Crockett

Good title, ill await your reply 🤣

Collapse
tobi418 profile image
tobi418

There is new and better alternative in ES7 named Observable.

Collapse
damcosset profile image
Damien Cosset Author

Ha, don't know about that one. I'll need to explore that.