TryBugHunt

Async bugs and race conditions in JavaScript

A missing await, results in the wrong order, and updates that overwrite each other under load.

14 free challenges14 JavaScript

What are async bugs and race conditions?

Code that assumes work has finished when it has not. The symptom is a Promise where a value should be, results arriving in an unexpected order, or state that is correct with one user and wrong with several.

Why it happens

An async function always returns a promise, so forgetting a single await hands the promise itself onward. `return somePromise` inside a try block escapes before it can reject, so the catch never runs. And every await is a point where other code can run — if you read a value, await, then write it back, anything that ran in the gap is silently overwritten.

How to recognise it

  • "[object Promise]" or typeof 'object' where a value was expected.
  • It works with one item and breaks with several.
  • Output order changes between runs.
  • A try/catch around async work never catches anything.

Error guides caused by this pattern

All error guides →

In-depth guides for common exceptions and runtime errors triggered by async and race conditions, with working fixes and real-world examples:

How to fix it

Await everything that returns a promise, and use Promise.all when you need every result rather than the first. Never hold a copy of shared state across an await — read and write without yielding in between. Test concurrency with concurrent input; a sequential test cannot find a race.

Breaks vs Works Example

Breaks

// JavaScript: forEach does not await async functions
items.forEach(async (item) => {
  await processItem(item);
});
console.log('Done'); // Logs before items finish!

Works

// JavaScript: Promise.all waits for all concurrent operations
await Promise.all(items.map((item) => processItem(item)));
console.log('Done'); // Runs only after all items complete

💡 Standard array callbacks (forEach, map, filter) do not wait for returned promises. Use Promise.all or for...of.

Practise async and race conditions

Working code with one bug in it. Find it, fix it in the browser, and see the explanation. No account needed.

javascriptEasy
getName gives back undefined

getName(id) loads a user and gives back their name. Example: await getName(1) → "user1".

Async race condition

javascriptHard
Delayed double returns undefined

delayedDouble(n) waits a moment, then gives back n doubled. Calling code uses await. Example: await delayedDouble(5) → 10.

Async race condition

javascriptHard
3 people book the last seat

bookSeats(n) sends n booking requests at the same time for 1 remaining seat, and returns how many bookings succeeded. It should never be more than 1. Example: await bookSeats(3) → 1.

Async race condition

javascriptHard
Cached loader runs 3 times instead of once

countLoads(n) calls a cached loader n times at once and returns how many times the slow load really ran. It should always be 1. Example: await countLoads(3) → 1.

Async race condition

javascriptHard
Adding 4 votes at once keeps only the last

tallyVotes(votes) adds up every vote. The votes are counted at the same time with Promise.all. Example: await tallyVotes([1, 2, 3, 4]) → 10.

Async race condition

javascriptHard
try/catch doesn't catch the divide-by-zero error

safeDivide(a, b) returns a / b, or -1 if the division fails. Example: await safeDivide(10, 2) → 5, await safeDivide(10, 0) → -1.

Async race condition

javascriptHard
Async sum gives "[object Promise]3"

sumAsync(nums) adds up values that are fetched with an async function. Example: await sumAsync([1, 2, 3]) → 6.

Async race condition

javascriptHard
Names come back in finishing order, not input order

labelAll(items) returns the names in the same order as the input, however long each one takes. Example: await labelAll([{ name: "a", ms: 20 }, { name: "b", ms: 1 }]) → ["a", "b"].

Async race condition

javascriptHard
Doubling with forEach returns an empty list

doubleAll(nums) doubles every number with fetchDouble, which takes a moment, and returns the results. Example: await doubleAll([1, 2, 3]) → [2, 4, 6].

Async race condition

javascriptMedium
Names list is full of promises, not text

collectNames(ids) loads every name, then reports how many there are and the type of the first one. Example: await collectNames([1, 2]) → "2-string".

Async race condition

javascriptMedium
Loading 3 items returns only one

loadAll(ids) loads every id and returns all the results in order. Example: await loadAll([1, 2, 3]) → [10, 20, 30].

Async race condition

javascriptMedium
Doubling after a delay returns undefined

doubleLater(value) waits a moment, then gives back value × 2. Example: await doubleLater(4) → 8.

Async race condition

javascriptMedium
Total shows "[object Promise]" instead of 6

sumDelayed(nums) adds up numbers that each arrive after a short delay. Example: await sumDelayed([1, 2, 3]) → 6.

Async race condition

javascriptMedium
Word lengths come back as promises, not numbers

wordLengths(words) returns the length of each word. Example: await wordLengths(["abc", "defg"]) → [3, 4].

Async race condition

Stuck on your own code?

Paste it into the visualizer and watch it run line by line, with every variable at every step. Free, and it runs in your browser.

Other bug patterns