Asynchronous JavaScript
Practice async/await with a loading sequence
Predict execution order, refactor a promise chain, handle rejection, and run independent work together in a small JavaScript exercise.
In the promises lesson we chained asynchronous steps. Now we’ll rewrite a loading sequence with async and await, then check what runs while it waits.
Keep the async/await guide open if you need the syntax reference. This exercise uses timers so you can run it without an API or a network connection.
Predict the output before running it
Save this as loading.mjs and run node loading.mjs. You can also paste it into your browser Console:
const wait = (value, delay) => new Promise(resolve => {
setTimeout(() => resolve(value), delay)
})
async function loadProfile() {
console.log('Loading')
const user = await wait({ name: 'Ada' }, 100)
console.log(user.name)
return user
}
console.log('Before')
const result = loadProfile()
console.log('After')
result.then(user => console.log(`Ready: ${user.name}`))
Write your predicted lines down first. The result is Before, Loading, After, Ada, Ready: Ada.
The function starts immediately. At await, it gives control back to the caller. result is a promise, even though the function eventually returns a user object. Adding async does not move the function onto another thread.
Refactor dependent steps
Use the same wait helper. This chain loads a user before it can load that user’s posts:
const getUser = () => wait({ id: 7 }, 100)
const getPosts = id => wait([`Post by ${id}`], 100)
function loadDashboard() {
return getUser()
.then(user => getPosts(user.id))
.then(posts => ({ posts }))
}
Rewrite loadDashboard as an async function. Keep its return value and dependency order. Your implementation should match this:
async function loadDashboard() {
const user = await getUser()
const posts = await getPosts(user.id)
return { posts }
}
Call loadDashboard().then(console.log). Both versions produce { posts: ['Post by 7'] }. Calling getPosts before you have the user would change the program.
Make a failure visible
Temporarily replace getPosts with a rejected promise:
const getPosts = () => Promise.reject(new Error('Posts unavailable'))
Put the call inside an async main function and catch its failure:
async function main() {
try {
console.log(await loadDashboard())
} catch (error) {
console.log(error.message)
}
}
main()
You should get Posts unavailable and no dashboard object. An uncaught error inside an async function rejects its returned promise. A try/catch around an awaited call handles it.
As a check, remove await inside main. The catch no longer handles the later rejection. Restore it before continuing.
Start independent work together
Restore the working getPosts. A settings lookup doesn’t depend on the user, so it can start while the dashboard loads:
const getSettings = () => wait({ theme: 'dark' }, 100)
async function loadPage() {
const [dashboard, settings] = await Promise.all([
loadDashboard(),
getSettings()
])
return { dashboard, settings }
}
loadPage().then(console.log).catch(console.error)
Before running it, draw the order of calls. The settings request overlaps the user lookup; the posts lookup still waits for the user. Promise.all() preserves the input order in its results and rejects if an input rejects. It does not cancel the other work.
You can move on when you can explain each wait and which promise carries a failure. The next event loop lesson explains how JavaScript resumes this work.
Lesson completed