Promises: The Basics

(Psst! all the code for this post can be found here! Pass it on...)

So there's this thing we've done forever in JavaScript, called callbacks:

const takeCallback = (f) => f()

takeCallback(() => console.log('potatoes!!'))  
//prints potatoes!!

Since functions are first-class in JavaScript, we can hand a function to another function, and that second function can call it. This is how we've handled AJAX for ages.

Here's how I used to do it in jQuery:

$.ajax({
  url: '/ajax',
  success: data => console.log(data),
  error: console.error
})
//prints { message: "This comes from an AJAX call!" }

I'm giving $.ajax an object containing a url and two functions (aka two callbacks). The success function will be called if the AJAX request is a success. The error callback will execute if the AJAX call doesn't complete.

All this is fine, but what if I want to chain network calls? I'll get a callback within a callback within a call back, the pyramid of doom. What if you want some kind of reasonable error handling in that pyramid? It's not pretty.

Enter promises:

const $get = path =>  
    new Promise(
        (resolve, reject) =>
            $.ajax({
              url: path,
              success: resolve,
              error: reject
            }))

$get('/ajax').then(data => console.log(data))
//prints { message: "This comes from an AJAX call!" }

I usually don't use the new keyword because I think it's icky, but I've gotta if I want to make a new promise object. Promise is a special type of function called a "constructor", and for it to work properly you need to put new in front of it when you call it.

With that out of the way, Promise takes a function that takes two functions: resolve and reject.

resolve is called if whatever asynchronous dealy we're doing succeeds, and reject gets called if it doesn't succeed.

What you get when you call new Promise(someFunc) is an object called a promise. One of the properties a promise is guaranteed to have is then, a function that takes the resolve function as it's argument.

Whatever function you give to .then will be called if the promise gets properly resolved.

$get('/ajax').then(data => console.log(data))
//prints { message: "This comes from an AJAX call!" }

A promise also has a .catch function, the function you give .catch will be called if the promise does not get resolved:

$get('/bananas')
    .then(() =>
        console.log(
            'this never executes because'
            + 'there is no \'/bananas\''))
    .catch(error => console.log(error.responseText))
//prints Cannot GET /bananas
//so sad

That's the basic idea behind promises. A promise is an object that deals with some asynchronous behavior. It has a .then and a .catch. The function you give .then is called if the behavior succeeds. The function you give .catch is called if the behavior fails.

Hey, here's one of those pyramids of doom everybody is talking about:

$.ajax({
  url: '/heroes',
  success: results =>
    $.ajax({
        url: `/sidekicks/${
            R.pipe(
                R.prop('heroes'),
                R.find(hero => hero.name === 'Batman'),
                R.prop('id'))
                    (results)}`,
        success: results => $.ajax({
            url: `/firstappearance/${
                R.pipe(
                    R.prop('side_kicks'),
                    R.last,
                    R.prop('id'))
                        (results)}`,
            success: firstappearance => 
                console.log(firstappearance),
            error: console.error
        }),
        error: console.error
    }),
  error: console.error
})

Here's what it prints to the console:

{
    'id': 11,
    'name': 'Robin',
    'real_name': 'Carrie Kelly',
    'first_appearance': {
        'title': 'Batman: The Dark Knight Returns',
        'year': 1986
    }
}

We're doing 3 AJAX calls in succession. Each one has to happen right after the other. The only way to do this used to be by putting new $.ajax calls inside your $.ajax callbacks.

Here's that same behavior using promises:

$get('/heroes')
    .then(results =>
        $get(`/sidekicks/${
            R.pipe(
                R.prop('heroes'),
                R.find(hero => hero.name === 'Batman'),
                R.prop('id'))
                    (results)}`))
    .then(results =>
        $get(`/firstappearance/${
            R.pipe(
                R.prop('side_kicks'),
                R.last,
                R.prop('id'))
                    (results)}`))
    .then(firstappearance =>
        console.log(firstappearance))
    .catch(console.error)

This is a promise chain. It's possible because of a really nice feature of how promises work.

If the function you give .then or .catch returns a promise, you can just chain another .then or .catch right after it.

As long as the functions you give .then and .catch return promises, you can chain .then's and .catch's as long as you want.

This makes error handling really easy, since even a regular error will trickle down to whatever .catch comes next:

$get('/ajax')
    .then(function () {
        throw 'It\'s my party'
            + ' and I\'ll cry if I want to!' 
    })
    .then(() => console.log('this never executes'))
    .catch(complaint => console.log(complaint))
//prints It's my party and I'll cry if I want to!

With the pyramid we had to place our error function in three different places, and it still wouldn't have handled a regular thrown error like this. Here we just have one .catch to handle everything.

You can even put a .then after a .catch. Kinda weird but also useful if you still want to do things after an error happens:

$get('/bananas')
    .then(() => console.log('this never prints'))
    .catch(() =>
        new Promise((resolve, reject) =>
            resolve('returned from catch!')))
    .then(fromCatch => console.log(fromCatch))
// prints returned from catch!

Pretty rad I feel. But there's even more! Stay tuned for more promise-y goodness next week.

Looking for a software developer?