Javascript Promises

What are Promises?

Promises help us manage asynchronous control flow.

 

When something won't return a value immediately, we can Promise to return its value when it's ready.

No async, no problems.

// Get some text, transform it to uppercase and inject it into the DOM

function getText() {
    return `Super clean this morning with a nice offshore and around 
            3ft or swell to play in. In fact its clean surf at 13th 
            and townies….clean surf from Posso’s to Bells…too much choice…
            mmm…`;
}

let text = getText();
let uppercase = text.toUpperCase();
document.getQuerySelector('#target').textContent = uppercase;

// done. beer time. 

But sometimes...

// Fetch some text from a remote source, 
// transform it to uppercase and inject it into the DOM

let text = fetchMyText('rastasurfboards.com.au/Rasta/SurfReport.aspx');
let uppercase = text.toUpperCase();

// Oh Snap, TypeError! Cannot read property 'toUpperCase' of undefined.

Whut?

fetchMyText() is asynchronous. 

It doesn't return a value now, the value becomes available later.

// Fetch some text from a remote source, 
// transform it to uppercase and inject it into the DOM

let text = fetchMyText('rastasurfboards.com.au/Rasta/SurfReport.aspx');
let uppercase = text.toUpperCase();

// Oh Snap, TypeError! Cannot read property 'toUpperCase' of undefined.

Asynchronous Javascript

  • JS is single-threaded
  • Blocks of code are added to the Event Loop and executed in sequence
  • It doesn't block
  • The host environment (Node, browser) can do some things asynchronously, like:
    • User Interaction
    • IO
    • Network requests

Asynchronous Javascript

When we're working with asynchronous APIs like 

setTimeout and fetch, we're basically asking the host:

 

  • I'm waiting for something to happen
  • Meanwhile, do your thing
  • When the thing I'm waiting for happens, do something

Do something?

Something like a callback. A function that gets called back later.

Async with callbacks

// Fetch some text from a remote source, 
// transform it to uppercase and inject it into the DOM

let text = fetchMyText('rastasurfboards.com.au/report', function(error, data){

    // our callback function gets added to the event loop and executed
    // when the request is finished and the data is available. 

    let uppercased = data.toUpperCase();
    document.querySelector('#target').textContent = uppercased;

});


// meanwhile, whatever's below has been executed...

What's the problem?

  • Callback hell, or the pyramid of dooooom
  • They quickly become difficult to reason about
  • There are some common patterns, but there's no spec.
  • This can be problematic because of inversion of control and trust issues

That's right, PYRAMID OF DOOM.

I'm sold. Let's do it.

  • ES2015 (aka ES6) gives us Native Promises.
    • Useable now in Chrome, Firefox, Node
  • Otherwise, it's polyfill time
    • Bluebird
      • Fast
      • Loads of extra features
      • Compatible with the ES2015 spec

Let's make a Promise

function promiseMyText(url){

    // wrap our asynchronous call in a Promise
    return new Promise(function(resolve, reject) {

        fetchMyText(url, function(err, data) {

            // a promise has 2 outcomes...

            // there was an error, reject the promise
            if(err) {
               return reject(err);
            }

            // we got our data, resolve the promise
            if(data) {
                return resolve(data);
            }

        });
    });
});

And now let's use it

promiseMyText('rastasurfboards.com/report')
.then(function(data) {
    let uppercased = data.toUpperCase();
    document.querySelector('#target').textContent = uppercased;
}).catch(function(err) {
    alert(err);
});

Let's break that down.

// promiseMyText() is going to return us a Promise

let promise = promiseMyText('rastasurfboards.com/report');


// the most important thing is that our Promise is *thenable*.
// it implements a then() method that we give a callback to.

promise.then(function(data) {
  // this fires when our promise has resolved
  // and is passed the fulfilment value (data).

  let uppercased = data.toUpperCase();
  document.querySelector('#target').textContent = uppercased;

});

// Promises also reveal a catch() method for handling rejection

promise.catch(function(error) {
  alert(error);
});

Yeah, but...

Isn't that just a callback?

The Rules

  • Either your then() or your catch() handler will fire.
  • They will fire once and only once. 
  • They will be called with a single value, always. 
  • If anything blows up, your catch() will get it.

Fun stuff: Chaining

// When you're working with a promise, calling then() or catch() 
// starts up a sequential, chainable sequence.
// The return value of your fulfilment handler will be passed
// to the next step in the chain. 

fetchUsername()
.then(function(username) {
  return fetchUserDetails(username);
}).then(function(userDetails) {
  return fetchUserAvatar(userDetails.id);
}).then(function(avatar) {
    doSomethingWithAnAvatar(avatar);
})
.catch(function(error) {
    alert(error);
});

Fun stuff: Chaining

// Your fulfilment callback can return any type of value you want.

getMyPromise()
.then(function(x){

    // we can just return a string
    return 'Stringy McStringface';

}).then(function(y) {
    // y === Stringy McStringface

    // we can return another promise
    return getAnotherPromise();

}).then(function(z) {
    // z is the resolved value of our last promise

    // we can also return a 'thenable'
    return getAFakePromise();
}).then(function(q) {
    // and the Promise will unwrap it for us and pass
    // its resolved value.
}).catch(function(err) {
    alert('oh snap');
});

OTT Chaining

// You can even return nested chains. Even if maybe you shouldn't.

promise.then(function(x) {
    return getAPromise()
        .then(function(y) {
            return 'foobar';
        }).then(function(z) {
            return getAnotherPromise(z);
        });
}).then(function(q) {
    // q will be the resolved result of getAnotherPromise(z)
}).catch(function(err) {
    alert('aw shit');
});

Async in parallel with .all

// If you have more than one Promise that you're waiting for 
// and you want to do something when they've all resolved, 
// you can use Promise.all()

Promise.all([fetchSomething('foo'), fetchSomething('bar'), fetchSomething('qux')])
.then(function(results) {
    // results is an iterable, same size as the one you passed, 
    // in the same order, passing the resolved values.
    // results === [
    //    fetchSomething('foo'),
    //    fetchSomething('bar'),
    //    fetchSomething('qux')
    // ]

.catch(function(err) {
    // the error/rejection thrown by whichever failed.
    // it will only be one, because once the first one 
    // fails it's game over.
});

Fight Fight Fight Fight

// Maybe you want to do a few async operations 
// but only respond to the one that happens first.
// Remember, our fulfilment callback will only ever fire once.

Promise.race([getPromise('foo'), getPromise('bar'), getPromise('qux'])
.then(function(result) {
    // result is the resolved value of whichever finished first.
    // it's up to you to figure out which one it was.
}).catch(function(err) {
    // error/rejection from anything that failed BEFORE 
    // you got a successful resolution.
});

Fight Fight Fight Fight

// For example, let's show our users a notification that 
// hides itself after 10 seconds.

let clickCloseButton = new Promise(function(resolve, reject) {
    $(closeButton).on('click', resolve);
});

let waitTenSeconds = new Promise(function(resolve, reject) {
    setTimeout(resolve, 10000);
});


Promise.race([clickCloseButton, waitTenSeconds])
.then(function(result) {
    myNotification.dismiss();
})
.catch(function(err) {
    alert('OH NO');
});

Sync? Async? Don't know?

// Promises are also super useful when running code that 
// might be async or might be sync. 
// If you wrap it up in a Promise, it doesn't matter. 

function randomChance(){
    return new Promise(function(resolve, reject) {

        if (Math.random() > 0.5) {
            resolve('yolo');
        } else {
            setTimeout(function() {
                resolve('yiew');
            }, 5000);
        }   
    });
}

randomChance()
.then(function(){
    // it's all the same to me.
}).catch(errorHandler);

Shortcuts

// You can quickly create a resolved/rejected Promise:

var promise = Promise.resolve(true);

promise.then(function(x) {
    x === true; // yeah dawg
});

var badPromise = Promise.reject(false);

promise.catch(function(err) {
    err === false; // mm-hmm
});

Error handling

This can look a bit confusing at first, but there's just a couple of things to keep in mind.

 

Plus! The good browsers are now starting to improve their tools for error handling and debugging.

Error handling

// Remember that a Promise has two outcomes; resolution or rejection.
// A Promise can be rejected in two ways.

// 1. We manually call the reject callback.
let myPromise = new Promise(function(resolve, reject) {
    reject('sif m8 get out of here');
});

myPromise.then(function() {
    // never gets called
}).catch(function(err) {
    // err === 'sif m8 get out of here'
});

Error handling

// Remember that a Promise has two outcomes; resolution or rejection.
// A Promise can be rejected in two ways.

// 2. Something explodes
let myShittyBrokenPromise = new Promise(function(resolve, reject) {
    madeUpThingThatsUndefined.bogusFunction();
    resolve('yiewwww');
});

myPromise.then(function() {
    // never gets called
}).catch(function(err) {
    // ReferenceError: madeUpThingThatsUndefined is not defined
});

Error handling

Your Promise is essentially running inside a try/catch.

The key is to remember you must catch().

 

If you don't catch, you're leaving it to chance. 

Polyfills like Bluebird will attempt to warn you, and as of really recently, Chrome and Firefox will too. 

 

But there are plenty of scenarios where the promise will silently swallow your exception/rejection. 

Error handling

And even this isn't bullet-proof!

For example (shout out to @ptim) :

promise.then(handleMyPromise)
.catch(function(err) {
    handleMyError(err);
});

Error handling

And even this isn't bullet-proof!

For example (shout out to @ptim) :

promise.then(handleMyPromise)
.catch(function(err) {
    handleMyError(err); // assume this breaks, you're basically doing
    throw new Error('nah thats busted too');
});

// OMG BUT WHO IS CATCHING THE CATCHERS 

Error handling

// Simple error handling

promiseMeSomething()
.then(promiseMeSomething)    // error happens here
.then(promiseMeSomething)    // <= so these... 
.then(promiseMeSomething)    // <= never...
.then(promiseMeSomething)    // <= fire.
.catch(function(err) {
    // somebody dun goofed
    console.error(err.stack);
});

Error handling

// Make it a bit smarter.
// A rejection/exception is always going to go 
// to the next catch().

promiseMeSomething()
.then(promiseMeSomething)
.catch(function(err) {
    // this failed here, but i don't care.
    // i just want to tell somebody.
    emailMitch('m8 shes borked again');
})
.then(promiseMeSomething) // exception was handled, so on we go.
.then(promiseMeSomething)
.then(promiseMeSomething)
.catch(function(err) {
    // somebody dun goofed
    console.error(err.stack);
});

Always return a Promise

// A function that returns a Promise should always return a Promise!

function parseUrlAndFetchText(url){ 
    let parsed = parseUrl(url);
    return Promise.resolve(fetchText(parsed));
});


parseUrlAndFetchText('rastasurfboards.com.au/surfreport')
.then(function(surfReport){
    alert(surfReport);
}).catch(function(err) {
    console.error(err.stack);
    alert('boom!');
});

Promisification

At this stage you're all obviously thinking,

 

Wow, what a compelling and well thought-out presentation. I can't wait to go back upstairs and start using Promises.

 

 

Promisification

Modern libraries and APIs

 

Most are now implementing Promises or Promise-like interfaces (thenables.)

 

For example, the fetch API is promisified out of the box.

Promisification

Modern-ish libraries & thenables

 

Some modern libraries will implement thenables;

they return objects with a then() callback.

 

You can wrap these in Promise.resolve() to return a legit, real-deal Promise.

Promisification

// The ES2015 Promises spec and the better polyfills 
// like Bluebird guarantee us access to all the cool 
// parts of promises. So when dealing with thenables, 
// we can coerce them into real Promises.

import fetchMaster420 from 'fetchMaster420';

// fetchMaster420 returns a thenable. 
// action: ask Daryl about duck typing.

// We can 'upgrade' this to a real Promise like all our 
// other promises, by wrapping it. 
// Remember, Promises will unwrap a fulfilment callback 
// and return the resolved value. 

Promise.resolve(fetchMaster420('rastasurfboards.com.au/report')
.then(blah).catch(freakout);






Promisification

The rest

 

Bluebird gives you tools like Promise.Promisify that will take a function with a standard node-style argument signature and wrap it automatically. 

 

Otherwise, it's not hard to do it yourself.

Promisification

// Let's use a standard Node utility as an example.

import Promise from 'bluebird';
import { readFile } from 'fs';

const promiseReadFile = Bluebird.promisify(readFile);

// the callback way;
// last argument is a callback with arguments error, data.
readFile('foobar.txt', function(err, data) {
    if (err) {
        return ohShit(err);
    }

    doSomethingWith(data);
});

// the Promisified way :)
promiseReadFile('foobar.txt')
.then(doSomethingWith)
.catch(ohShit);
Made with Slides.com