Unlike a lot of other languages, JavaScript is single-threaded which means it can only process one command at a time. We often need to get data from other sources, be it from a database, server or the user and I'm sure you can imagine how bad our applications would be if we had to pause the execution of our program each time we were waiting. Promises solve this problem for us, we request some data from wherever we're getting it from and we set the actions which we want to run once the data is returned. This leaves our single-thread free to carry on with other tasks in the meantime.
Until quite recently it was necessary to use a third-party library for promises (jQuery's Deferred was the most popular) but since ES6 they have been native to JS. Here's the basic pattern:
const promise = new Promise(resolve =>
setTimeout(() => resolve('I waited for 2 seconds'), 2000)
)
promise.then(message => console.log(message))
// I waited for 2 seconds
First we create a new promise
object from the Promise
constructor. This takes a function as an argument which is called with a function to execute when you want the promise to be resolved. I have just used a setTimeout
in this example to keep things simple but you would normally make calls to an API or query a database (in Node) here. After that we can use Promise's then
method to execute something when the promise is resolved.
Error handling
So that's how you use promises... As long as nothing goes wrong! With the above code there isn't anything in place to handle the potential outcome of the API returning an error status. Fortunately, handling errors is pretty straightforward:
const promise = new Promise((resolve, reject) =>
setTimeout(() => {
const chaos = Math.random()
if(chaos > 0.5) {
reject('Too much chaos!')
} else {
resolve('I waited for 2 seconds')
}
}, 2000)
)
promise.then(
message => console.log(message),
errorMessage => console.error(errorMessage)
)
The function called in the Promise
constructor is actually called with two callback functions; the second being one for rejecting the promise. In much the same way, then
also accepts a second callback which executes if the promise is rejected.
Promise chaining
It is possible to chain then
functions after a promise. This is also a good place to introduce you to catch
. When chaining, it is usually the case that you will only use then
for handling promises, a catch
can be added to the end of the chain to catch an error thrown from any point preceding it.
Here we will get our promise from another source; I quite often use Axios to make HTTP requests. Their API is really simple, to make a GET request you just write axios.get('api/endpoint/url')
which returns a promise that resolves when the data is returned from the server.
axios.get('api/endpoint/url')
.then(response =>
saveData(response)
axios.get('api/endpoint/url2')
)
.then(response2 =>
saveData(reponse2)
)
.catch(error =>
handleError(error)
)
Here we call our imaginary saveData
function with the response from the first API call, then make another request to a different endpoint. If an error is thrown anywhere in this chain it will be caught by the catch
function which passes the error object to another imaginary function - handleError
.
Promise also has a finally
method which can be added to the chain, it's pretty handy for spinners!
showSpinner()
axios.get('api/endpoint/url')
.then(response =>
saveData(response)
axios.get('api/endpoint/url2')
)
.then(response2 =>
saveData(reponse2)
)
.catch(error =>
handleError(error)
)
.finally(() =>
hideSpiner()
)
Async/await
There is now (since ES2017) a cleaner way than chaining then
functions on a promise. The above could be written using async/await like so:
async function getData() {
const data1 = await axios.get('api/endpoint/url')
saveData(data1)
const data2 = await axios.get('api/endpoint/url2')
saveData(data2)
}
We prepend the async
keyword to the function declaration and then use await
anywhere we want to pause execution to wait for a promise to resolve.
Unfortunately, error handling with async/await is not as clean as before and the only way (that I know of) to achieve the same level of error handling as above is to wrap everything in a try/catch
block:
async function getData() {
try {
const data1 = await axios.get('api/endpoint/url')
saveData(data1)
const data2 = await axios.get('api/endpoint/url2')
saveData(data2)
} catch(error) {
handleError(error)
}
}
Asynchronous programming
The examples above work perfectly well but, unless we need to wait for the result of the first call so that we can use the data in the response to make the second call, it is a very inefficient way of programming. I can illustrate this with some code.
First, let's create a mock HTTP GET method:
function get(response, delay) {
return new Promise(resolve => {
setTimeout(() => resolve(response), delay * 1000)
})
}
This function just returns the response
which we set with the first argument after waiting for amount of time set with the second argument, delay
. We also need to be able to time the test cases we're going to write so let's make a stopwatch:
const stopwatch = (() => {
let start, end
return {
start() {
start = Date.now()
},
stop() {
end = Date.now()
return (end - start) / 1000
}
}
})()
I've used a closure here, if you're not clued up on closures then you should check out my post about them here. Next, we'll recreate the functions from above:
const testCases = {
one() {
return new Promise(resolve => {
const responses = []
get('three', 3)
.then(res => responses.push(res))
.then(() => get('two', 2))
.then(res => responses.push(res))
.then(() => get('one', 1))
.then(res => responses.push(res))
.then(() => {
const output =responses.join(' ')
resolve(output)
})
})
},
two() {
return new Promise(async resolve => {
const first = await get('three', 3)
const second = await get('two', 2)
const third = await get('one', 1)
const output = `${first} ${second} ${third}`
resolve(output)
})
}
}
Hopefully you can recognise the code from earlier, only here we are making three get
requests instead of two. testCases.one
pushes all of the data into an array whereas testCases.two
uses await
to wait for the promises to resolve before assigning the data to local variables. Notice that we create a new Promise
and then make the get
request within which also returns a promise each time. When we have received all of our data we resolve our 'master' promise. So now, all that remains to do is add the code which runs these tests:
async function runTest(testCase) {
let result, time;
stopwatch.start()
result = await testCases[testCase]()
time = stopwatch.stop()
console.log('Result: %s, Time: %s seconds', result, time)
}
This function is called with the name of the test we want to run, it starts the stopwatch and then runs the test. When the test's promise is resolved the stopwatch is stopped and the results are logged to the console. Let's run our tests:
runTest('one') // Result: three two one, Time: 6.003 seconds
runTest('two') // Result: three two one, Time: 6.004 seconds
So you can see that both of our functions took six seconds to run, this is because we have written our code synchronously; each line is executed in order and will wait for the previous lines to complete before running itself. We can instead play to JavaScript's strengths and write our code asynchronously. We'll add a third test to our testCases
object:
three() {
return new Promise(resolve => {
const responses = []
function check() {
if(responses.length > 2) {
resolve(responses.join(' '))
}
}
get('three', 3).then(res => {
responses.push(res)
check()
})
get('two', 2).then(res => {
responses.push(res)
check()
})
get('one', 1).then(res => {
responses.push(res)
check()
})
})
}
There's a bit more going on with this function. First we initialise our empty responses
array, then we've added a function called check
. Following that we make our three get
requests as before, only this time we call check
each time one resolves. check
looks at the length of our responses
array and, when our array contains the three responses, it resolves our 'master' promise. Let's see how it does:
runTest('three') // Result: one two three, Time: 3.002 seconds
Half the time, and you can see that the order of our responses has changed, our code is running asynchronously!
Promise.all()
There is a better way to write test-case number three so that we don't need the check
function. We can also put our responses back into the same order as the other tests; in the real world this probably isn't important but let's do it anyway!
Time for a confession, I have forgotten to mention something very important about promises. Promises are always in one of three states. When you first create a promise it is in a "pending" state, it is then transitioned into either a "resolved" or "rejected" state. Once a promise has reached "resolved" or "rejected" it cannot go back to "pending". If you want to know which state a promise is in you can call Promise.state()
, this is pretty useful when debugging as you can set a breakpoint and run this in the console (I might do a post on debugging in Chrome soon).
Here's test-case number four:
four() {
return new Promise(resolve => {
const responses = []
responses.push(get('three', 3))
responses.push(get('two', 2))
responses.push(get('one', 1))
Promise.all(responses)
.then(values => {
const output = values.join(' ')
resolve(output)
})
})
}
In this function we push the returned promises from the get
call into the responses
array straight away. These promises are in a "pending" state and will act as placeholders, meaning that the results will be in the same order as our first two test-cases. Instead of checking the length of the responses
array each time a promise resolves we can use Promise's all
method, which itself returns a promise that resolves when all of the promises in the array resolve. This is much better than before because we no longer need to know how many promises we are waiting for:
runTest('four') // Result: three two one, Time: 3.003 seconds
Three seconds again and this time the results are in the correct order. Here's the running code from above if you would like to run the tests yourself:
Hopefully you found this post useful, thanks for taking the time to read to the end. If you have any comments, corrections or questions then you can contact me on Twitter. 👍
Top comments (0)