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.
// 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.
// 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.
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.
When we're working with asynchronous APIs like
setTimeout and fetch, we're basically asking the host:
Something like a callback. A function that gets called back later.
// 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...
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);
}
});
});
});
promiseMyText('rastasurfboards.com/report')
.then(function(data) {
let uppercased = data.toUpperCase();
document.querySelector('#target').textContent = uppercased;
}).catch(function(err) {
alert(err);
});
// 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);
});
Isn't that just a callback?
// 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);
});
// 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');
});
// 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');
});
// 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.
});
// 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.
});
// 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');
});
// 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);
// 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
});
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.
// 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'
});
// 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
});
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.
And even this isn't bullet-proof!
For example (shout out to @ptim) :
promise.then(handleMyPromise)
.catch(function(err) {
handleMyError(err);
});
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
// 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);
});
// 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);
});
// 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!');
});
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.
Most are now implementing Promises or Promise-like interfaces (thenables.)
For example, the fetch API is promisified out of the box.
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.
// 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);
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.
// 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);