Volume 2

Q111: async await Desugaring and Error Flow

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

What Interviewers Want To Evaluate

This question checks whether you understand async and await as syntax over promises. Interviewers want promise return behavior, await suspension, error propagation, try/catch, sequential vs parallel execution, and how async functions interact with the microtask queue.

Senior answers should connect this to loading states, request waterfalls, unhandled rejections, and readable async control flow.

Short Interview Answer

An async function always returns a promise. Returning a value fulfills that promise, throwing an error rejects it, and awaiting a promise pauses the async function until the awaited value settles. await does not block the whole JavaScript thread; it suspends that async function and resumes it later through promise microtask scheduling. try/catch around await handles rejected promises like synchronous exceptions.

Detailed Interview Answer

async and await make promise code easier to read, but they do not remove promise mechanics.

async function getUserName() {
  const user = await fetchUser();
  return user.name;
}

This function returns a promise immediately. The body can suspend at await, then continue after the awaited promise settles.

async Always Returns a Promise

Even when an async function returns a plain value, callers receive a promise.

async function answer() {
  return 42;
}

answer().then(console.log); // 42

The returned value becomes the fulfillment value of the promise.

Throwing Rejects

Throwing inside an async function rejects the returned promise.

async function loadUser(id) {
  if (!id) {
    throw new Error("Missing id");
  }

  return fetchUser(id);
}

This is why callers must use await, try/catch, or .catch.

What await Does

await waits for a value to resolve. If the value is not a promise, it is still wrapped through promise resolution.

async function run() {
  const value = await 10;
  console.log(value);
}

The continuation still happens asynchronously.

await and Rejections

If an awaited promise rejects, await throws the rejection reason at that line.

async function loadProfile() {
  try {
    const profile = await fetchProfile();
    return profile;
  } catch (error) {
    console.error("Could not load profile", error);
    throw error;
  }
}

This lets async promise errors be handled with normal try/catch syntax.

Sequential Execution

Multiple awaits in a row are sequential.

const user = await fetchUser();
const settings = await fetchSettings(user.id);

This is correct when the second request depends on the first.

Accidental Waterfalls

Sequential awaits are inefficient when operations are independent.

const user = await fetchUser();
const permissions = await fetchPermissions();

If these do not depend on each other, this creates an unnecessary waterfall.

Parallel Execution

Use Promise.all for independent work.

const [user, permissions] = await Promise.all([
  fetchUser(),
  fetchPermissions(),
]);

This starts both operations before awaiting their combined result.

Desugaring Mental Model

This:

async function run() {
  const value = await task();
  return value * 2;
}

Behaves roughly like:

function run() {
  return Promise.resolve(task()).then((value) => value * 2);
}

The actual spec is more detailed, but this mental model is useful for interviews.

Microtask Resume

After an awaited promise settles, the async function resumes in a promise reaction microtask.

console.log("A");

async function run() {
  await null;
  console.log("B");
}

run();
console.log("C");

The output is:

A
C
B

Error Handling Patterns

Use try/catch when the current function owns the recovery path.

Use .catch at boundaries such as top-level event handlers, job runners, or logging wrappers.

button.addEventListener("click", () => {
  submitForm().catch(reportError);
});

Event listeners do not automatically wait for returned promises.

finally with async await

finally is useful for cleanup.

async function submit() {
  setLoading(true);

  try {
    await save();
  } finally {
    setLoading(false);
  }
}

This cleanup runs for both success and failure.

Common Mistakes

A common mistake is using await inside a loop when work could run concurrently.

for (const id of ids) {
  await fetchUser(id);
}

Sometimes this is correct for rate limits or order. If not, use Promise.all.

Another mistake is forgetting to handle async errors at the boundary.

Senior Trade-Offs

async and await improve readability, but they can hide concurrency decisions. Senior engineers make dependency ordering explicit.

Use sequential awaits for dependent steps. Use promise combinators for independent steps. Add cancellation where the work can become obsolete.

Debugging Workflow

When async behavior is wrong:

check whether the async function return value is awaited
check missing try/catch or catch boundary
check accidental request waterfalls
check Promise.all vs allSettled needs
check loading cleanup in finally
check unhandled rejection logs
check cancellation with AbortController

Interview Framing

Start by saying async returns a promise and await pauses only that function. Then show error flow and sequential vs parallel examples.

Revision Notes

async and await are promise syntax. Values fulfill, throws reject, and awaited continuations resume through microtasks.

Key Takeaways

The main senior skill is not syntax. It is making async ownership, error recovery, concurrency, and cleanup explicit.

Follow-Up Questions

  1. What does an async function return?
  2. What happens when an async function throws?
  3. Does await block the JavaScript thread?
  4. How does await handle rejection?
  5. When should awaits be sequential?
  6. When should you use Promise.all?
  7. What causes request waterfalls?
  8. How do you handle errors in event handlers?
  9. Why is finally useful?
  10. How does await relate to microtasks?

How I Would Answer This In A Real Interview

I would explain that async functions return promises and await pauses only the async function. Then I would compare sequential and parallel awaits, show try/catch error handling, and mention that unhandled async boundaries must be handled deliberately.