How to return a response from an asynchronous call in JavaScript
Nick Scialli
March 18, 2023
Asynchronous JavaScript is one of the trickier aspects of the language. In this post, we’ll see how to handle the result of an asynchronous call.
Creating an example
Let’s create a contrived example. We’ll make a delayedRandom
function that returns a random number after a small delay. Here’s how that might look:
function delayedRandom(ms) {
setTimeout(() => {
return Math.random();
}, ms);
}
delayedRandom(100);
// How do we get the result?
In this function, we provide ms
, which stands for milliseconds. We use the setTimeout
function that will create a random number after the provided number of milliseconds.
When we call delayedRandom
; however, we realize we don’t know how to get the generated random number.
Callback functions
The first solution we’ll provide is the callback function. JavaScript has “first class functions,” which means we’re allowed to pass functions are arguments to other functions. That’s example what a callback function is— it’s a function we pass to another function. Inside the other function, we can exection the callback function whenever we want!
Let’s create a new function that we want to execute once the random number is generated:
function callback(number) {
console.log('The random number is: ' + number);
}
Now, we can pass this callback function to delayedRandom
and call it when the number is generated:
function delayedRandom(ms, cb) {
setTimeout(() => {
cb(Math.random());
}, ms);
}
function callback(number) {
console.log('The random number is: ' + number);
}
delayedRandom(100, callback);
If we execute this code, we’ll see the following logged to the console (with a different random number each time you run it):
The random number is: 0.6947937030841276
If this is confusing, don’t worry—you’re not alone in thinking so! It’s important to look at the exact arguments passed to delayedRandom
(100
and callback
) and see how they’re used in the function as ms
and cb
, respectively.
Promises
Callbacks can be a bit challenging to follow and can get messy when combining multiple callbacks together (this is generally referred to as callback hell). Promises are a built-in JavaScript object that can provide a friendlier way to handle asynchronous code.
Promises represent the eventual completion of an asynchronous task. Promises can either resolve, meaning the operation was successful, or reject, meaning there was an error.
Let’s convert our example above to use promises and then explore what’s going on:
function delayedRandom(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(Math.random());
}, ms);
});
}
delayedRandom(100).then((number) => {
console.log('The random number is: ' + number);
});
Here we return a new Promise
object from delayedRandom
. When instantiating the Promise
object, we pass a function that currently has one argument—resolve
. Calling resolve
will signal that the promise has been fulfilled. We see that the callback is moved inside a then
call chained on to delayedRandom
, which is what gets called when the promise is fulfilled.
I had previously mentioned that a promise can be rejected, so let’s explore that case. We can pretend that, if our random number is less than 0.1, the promise will reject.
function delayedRandom(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const num = Math.random();
if (num < 0.1) {
reject(num);
} else {
resolve(num);
}
}, ms);
});
}
delayedRandom(100)
.then((number) => {
console.log('The random number is: ' + number);
})
.catch((number) => {
console.error('Oh no, the number was ' + number);
});
We can see that errors hare handled with chaining catch
on to our returned promise object.
Conclusion
Callbacks and promises are a couple different ways we can handle asynchronous code in JavaScript. While this is a paradigm shift from many other languages, if you continue practicing you’ll soon find yourself doing really great things with async code.
Nick Scialli is a senior UI engineer at Microsoft.