How to Wait for a Function to Finish in JavaScript
JavaScript is asynchronous by nature, and many times you'll need to work with non-linear code. This is when you'll run into cases where you need to wait for one function to finish to start another.
In this tutorial, we'll take a look at three different solutions to help you address this problem. But first, let's understand how asynchronous code behaves.
How Asynchronous Code Works
To demonstrate how to solve the problem, let's create the problem first. Take the following code as an example, where we have two functions called one after the other:
const first = () => console.log('1')
const second = () => console.log('2')
first()
second()
This code is synchronous. The execution order is from top to bottom, and as expected, "one" and "two" are logged to the console. If we change the order and move the second
function call before the first
, the order will change accordingly. However, if we modify the code as follows, the order will also change.
const first = () => console.log('one')
const second = () => console.log('two')
setTimeout(() => first(), 0)
second()
Even though the setTimeout
is set with a timeout of 0 milliseconds, the second function call is still executed before the first one.
This happens because the setTimeout
function is pushed to the end of the call stack. As a result, all other function calls are executed first, regardless of the timeout duration.
Wait for Functions using Callbacks
The simplest way to resolve this is by using a callback function. Callbacks are functions passed as arguments to other functions, to be executed once an event has occurred or a task is finished.
To fix the previous example using a callback, we can pass the second
function as an argument to the first
function and call it at the very end of the first
function, like so:
const first = callback => {
console.log('one')
callback()
}
const second = () => console.log('two')
setTimeout(() => first(second), 0)
Now we've turned around the execution order to ensure that the second
function is only called after the first
function has been executed. By specifying the callback as an argument, we make the function flexible, allowing us to pass any function to be executed once the first
function runs.
If you need to execute multiple functions after finishing one, the cleanest solution is to use an array as the callback argument. This way, you can use a forEach
loop inside the original function to iterate through the other functions and call them sequentially:
const first = callbacks => {
console.log('one')
callbacks.forEach(callback => callback())
}
const second = () => console.log('two')
const third = () => console.log('three')
setTimeout(() => first([second, third]), 0)
The callback functions will be executed in the order they appear in the array. Callbacks are also commonly used for event listeners. The following code also demonstrates the use of a callback function:
// This function will only run when the event occurs
const onClick = () => console.log('clicked')
document.addEventListener('click', onClick)
Avoid nesting multiple callback functions within each other, as this can result in code that's difficult to maintain.
It's worth mentioning that when you have multiple functions dependent on each other, using callbacks might not be the optimal solution. This is because they can lead to a situation known as "callback hell", where functions become deeply nested.
Wait for Functions using Promises
The second solution for waiting until functions finish before executing another one involves the use of promises. Promises also help us avoid callback hell. They're also called thennables, because we can chain a callback function from them using a then
function. Consider the following example:
const first = () => {
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(result => {
console.log('one')
})
}
const second = () => console.log('two')
first()
second()
The Fetch API in JavaScript is promise-based. We chain two then
callbacks from the fetch
function:
- First, we convert the response object into JSON.
- Then, we consume the JSON response in the second
then
.
However, the problem with this approach is that the fetch
function is asynchronous. The "two" is logged immediately, while "one" is only logged after the response is returned from the server.
To fix this code, we need to return the fetch
call from the first
function. This means the return value of the first
function will be a promise, so we can chain a then
callback and call the second
function only when the first one has finished execution:
const first = () => {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(result => {
console.log('one')
})
}
const second = () => console.log('two')
first().then(() => {
second()
})
Now, let's consider a scenario where we have three different API calls, and we want to wait for all of them to finish before calling another function. This can be achieved using Promise.all
, which expects an array of promises to be passed:
const fetchPost = id => {
return fetch('https://jsonplaceholder.typicode.com/posts/' + id)
.then(response => response.json())
}
const first = fetchPost(1)
const second = fetchPost(2)
const third = fetchPost(3)
Promise.all([first, second, third]).then(results => {
// Execute function here after all calls finished
console.log(results);
});
The functions will be resolved in order, meaning the results
variable will be an array where the first item references the result of the first
function, the second item references the result of the second
function, and so on.
Wait for Functions using async/await
Lastly, we can also use the async
/await
keywords to wait for functions to finish execution before proceeding. This approach allows us to write asynchronous code in a synchronous manner, improving code readability and maintainability.
To rewrite the previous example using the async
/await
keywords, we can change the code in the following way:
const fetchPost = id => {
return fetch('https://jsonplaceholder.typicode.com/posts/' + id)
.then(response => response.json())
}
const waitFor = async () => {
const first = await fetchPost(1)
const second = await fetchPost(2)
const third = await fetchPost(3)
// Execute any remaining functions here
console.log(first, second, third)
}
waitFor()
To use the await keyword, we must mark the function as asynchronous using the async
keyword. Now these functions are executed in the order they appear in the code. Of course, if we want to avoid creating an extra function just to use await
in JavaScript, we can also use an IIFE:
(async () => {
const first = await fetchPost(1)
const second = await fetchPost(2)
const third = await fetchPost(3)
// Execute any remaining functions here
console.log(first, second, third)
})()
Conclusion
In conclusion, waiting for functions to finish execution can be done by either using callbacks, promises, or the async
/await
keyword. So, which one should you use?
- async/await: Use
async
/await
as the primary choice for writing code in a synchronous manner. This improves read- and maintainability. - Promises: When dealing with multiple concurrently executing calls, use
Promise.all
orPromise.race
. - Callbacks: Only use callback for simple cases or when using
async
/await
or promises are not applicable.
Is there anything you think this tutorial is missing? Let us know in the comments below! If you're interested in learning more about JavaScript, make sure to check out our roadmap below. Thank you for reading, happy coding! π¨βπ»
Rocket Launch Your Career
Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies: