asynchronous
code behavior
programming
duplicate question
debugging

Strange async code behavior

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

When asynchronous code behaves strangely, the bug is usually not randomness. It is usually a timing assumption that turned out to be false: a missing await, an operation running in parallel when you expected a sequence, or shared state being updated in an order you did not control.

Async Code Does Not Mean Sequential Code

In JavaScript, marking a function async does not make all statements in the program wait politely for one another. It only means that the function can pause at await points while the event loop continues doing other work.

javascript
1function delay(ms, value) {
2  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
3}
4
5async function demo() {
6  console.log("start");
7  const result = await delay(100, "done");
8  console.log(result);
9}
10
11demo();
12console.log("after call");

This prints start, then after call, then done. That ordering surprises people who still expect the call to block like synchronous code.

Missing await Is The Classic Bug

A function that returns a promise will keep running in the background if you forget to await it.

javascript
1async function loadUser() {
2  return delay(100, { name: "Ada" });
3}
4
5async function main() {
6  const userPromise = loadUser();
7  console.log(userPromise);
8}
9
10main();

Here userPromise is a promise object, not the resolved user. The fix is direct:

javascript
1async function main() {
2  const user = await loadUser();
3  console.log(user.name);
4}

map With Async Functions Does Not Await Automatically

Another common surprise is Array.map with an async callback. It returns an array of promises, not an array of resolved values.

javascript
1async function fetchNumber(n) {
2  return delay(50, n * 2);
3}
4
5async function main() {
6  const values = [1, 2, 3].map(async (n) => fetchNumber(n));
7  console.log(values);
8}
9
10main();

To resolve them, wrap the result with Promise.all.

javascript
1async function main() {
2  const values = await Promise.all(
3    [1, 2, 3].map(async (n) => fetchNumber(n))
4  );
5  console.log(values);
6}

That pattern makes the intended concurrency explicit.

Race Conditions Come From Shared State

Async bugs become harder when several operations read and write the same variable.

javascript
1let counter = 0;
2
3async function incrementSlowly() {
4  const snapshot = counter;
5  await delay(50);
6  counter = snapshot + 1;
7}
8
9async function main() {
10  await Promise.all([incrementSlowly(), incrementSlowly()]);
11  console.log(counter);
12}
13
14main();

You might expect 2, but the result can be 1 because both functions read the same starting value before either writes back. The bug is not in await itself; it is in the shared-state design.

Debugging By Logging Boundaries

When behavior seems weird, add logs at async boundaries rather than only at the start and end of a request.

javascript
1async function saveOrder() {
2  console.log("before insert");
3  await delay(100);
4  console.log("after insert");
5}

You want to observe when execution yields and resumes. That gives you a timeline instead of a guess.

Sequential And Parallel Code Need Different Shapes

If each operation depends on the previous one, use a loop with await inside.

javascript
1for (const id of [1, 2, 3]) {
2  const value = await fetchNumber(id);
3  console.log(value);
4}

If the operations are independent, use Promise.all.

javascript
const values = await Promise.all([1, 2, 3].map(fetchNumber));
console.log(values);

A lot of "strange" async behavior is just the wrong control-flow shape for the intended order.

Common Pitfalls

The most common issue is forgetting await and then operating on a promise instead of a resolved value. Another is assuming that map, forEach, or filter will wait for async callbacks by themselves. Shared mutable state is also a major source of race conditions, especially when multiple requests update the same variable. Finally, code can look sequential even when it is not, so logging and tests should verify actual execution order instead of relying on intuition.

Summary

  • Async code is usually strange only when the timing model is misunderstood.
  • Missing await often explains why a value is a promise instead of data.
  • 'Promise.all is the standard way to wait for multiple concurrent operations.'
  • Shared mutable state creates race conditions even when each function looks correct by itself.
  • Debugging async code gets easier when you log before and after await points.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.