Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. JavaScript
  3. JavaScript Async/Await Practice
JavaScript37 exercises

JavaScript Async/Await Practice

Practice async/await patterns: missing await bugs, Promise.all vs Promise.allSettled, why await fails in forEach, error handling, and AbortController cancellation.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Create a Promise that resolves to "Success".

On this page
  1. 1Async functions always return Promises
  2. 2Parallel vs sequential
  3. 3Promise combinators: choosing the right one
  4. Promise.all: fail-fast parallelism
  5. Promise.allSettled: partial success
  6. Promise.race: timeouts
  7. Promise.any: first success
  8. 4Why await doesn't work in forEach
  9. Fixes
  10. 5Cancellation with AbortController
  11. 6Unhandled rejections: what the warning means
  12. 7The return vs return await trap
  13. 8Quick reference
  14. 9References
Async functions always return PromisesParallel vs sequentialPromise combinators: choosing the right oneWhy await doesn't work in forEachCancellation with AbortControllerUnhandled rejections: what the warning meansThe return vs return await trapQuick referenceReferences

JavaScript's async/await makes asynchronous code look synchronous, but the rules are still Promise rules.

Remember:

  • An async function always returns a Promise
  • await unwraps a Promise value, and can only appear inside an async function (or at the top level of a module)
  • If you see Promise { <pending> } instead of data, you forgot an await

Practice focuses on missing awaits, accidental serialization, Promise combinator choices, and errors that never get handled.

Related JavaScript Topics
JavaScript Error HandlingJavaScript FunctionsJavaScript LoopsJavaScript Collections

Even return 42 inside an async function becomes Promise.resolve(42). If you see Promise { <pending> } instead of data, you forgot an await.

This single fact explains most async/await bugs:

async function getData() {
  return 42;  // Still returns Promise<42>, not 42
}

const result = getData();
console.log(result);  // Promise { 42 }, not 42!

const actual = await getData();
console.log(actual);  // 42

Rule: If a function is async, you must await it to get the value. If you see Promise { <pending> }, you forgot an await somewhere. For a printable quick reference of all async patterns, see the JavaScript async/await cheat sheet.


Ready to practice?

Start practicing JavaScript Async/Await with spaced repetition

Sequential awaits in a loop run one-at-a-time. For independent work, collect promises into an array and await Promise.all() for parallel execution.

Sequential execution runs fetches one after another (5s total for 5 fetches), while parallel execution with Promise.all runs all at once (1s total)

Sequential awaits in loops are a common performance mistake:

// SLOW: Each fetch waits for the previous one
for (const id of ids) {
  const user = await fetchUser(id);  // 1s each = 5s total for 5 users
  users.push(user);
}

// FAST: All fetches run simultaneously
const users = await Promise.all(
  ids.map(id => fetchUser(id))  // 1s total for 5 users
);

When to use sequential: Only when each iteration depends on the previous result, or you're rate-limiting intentionally.

When to use parallel: Independent operations that don't affect each other.


Promise.all fails fast on any rejection. Use allSettled when partial success is acceptable, race for timeouts, and any for first-success-wins.

Promise combinator comparison: all fails fast on any rejection, allSettled waits for all, race returns first settled, any returns first fulfilled

JavaScript has four Promise combinators. Pick the right one:

CombinatorReturns whenUse case
Promise.allAll fulfill (or first rejects)Parallel fetch, fail-fast
Promise.allSettledAll settle (fulfill or reject)Partial success OK
Promise.raceFirst settlesTimeout, first-response
Promise.anyFirst fulfills (or all reject)First success wins

Promise.all: fail-fast parallelism

try {
  const [user, posts, settings] = await Promise.all([
    fetchUser(id),
    fetchPosts(id),
    fetchSettings(id)
  ]);
} catch (err) {
  // If ANY request fails, we end up here
  // The other requests still complete, but we don't get their values
}

Promise.allSettled: partial success

const results = await Promise.allSettled([
  fetchUser(id),
  fetchPosts(id),    // This can fail...
  fetchSettings(id)  // ...without losing these
]);

// Each result has { status, value } or { status, reason }
for (const result of results) {
  if (result.status === 'fulfilled') {
    process(result.value);
  } else {
    logError(result.reason);
  }
}

Promise.race: timeouts

const fetchWithTimeout = async (url, ms) => {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([fetch(url), timeout]);
};

Promise.any: first success

// Try multiple CDN mirrors, use whichever responds first
try {
  const response = await Promise.any([
    fetch('https://cdn1.example.com/data'),
    fetch('https://cdn2.example.com/data'),
    fetch('https://cdn3.example.com/data')
  ]);
} catch (err) {
  // AggregateError: all mirrors failed
  console.log(err.errors);  // Array of all rejection reasons
}

forEach fires all async callbacks simultaneously and returns undefined immediately, so console.log runs before any callback finishes

// BUG: forEach doesn't wait for async callbacks
items.forEach(async (item) => {
  await processItem(item);  // These fire but...
});
console.log('Done!');  // ...this runs immediately!

Why it fails: forEach returns undefined, not a Promise. The async callbacks execute, but nothing awaits them.

Fixes

Sequential (for...of):

for (const item of items) {
  await processItem(item);
}
console.log('Done!');  // Runs after all items processed

Parallel (map + Promise.all):

await Promise.all(items.map(item => processItem(item)));
console.log('Done!');  // Runs after all items processed

AbortController flow: create controller, pass signal to fetch, call abort to cancel, fetch rejects with AbortError

Modern fetch supports cancellation via AbortController:

const controller = new AbortController();

// Start the request
const fetchPromise = fetch('/api/data', {
  signal: controller.signal
});

// Cancel after 5 seconds
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetchPromise;
  clearTimeout(timeoutId);
  return response.json();
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('Request was cancelled');
  } else {
    throw err;
  }
}

Common patterns:

  • Cancel on component unmount (React useEffect cleanup)
  • Cancel on user navigation
  • Cancel superseded requests (new search query cancels old one)

The event-driven nature of async code shares principles with the Observer design pattern, where subscribers react to events they've registered interest in.


When a Promise rejects and nothing catches it:

async function oops() {
  throw new Error('Something broke');
}

oops();  // No await, no .catch()
// UnhandledPromiseRejection warning!

In Node.js: Unhandled rejections crash the process in Node 15+.

In browsers: Logged to console, may break application state silently.

Fix: Always handle rejections. For a deeper look at error handling patterns across languages, see the Python vs JavaScript error handling comparison.

// Option 1: await + try/catch
try {
  await oops();
} catch (err) {
  handleError(err);
}

// Option 2: .catch()
oops().catch(handleError);

// BUG: Error bypasses the catch block!
async function fetchData() {
  try {
    return fetch('/api/data');  // Returns pending Promise
  } catch (err) {
    return fallback;  // Never runs on fetch error!
  }
}

// CORRECT: await ensures error is caught locally
async function fetchData() {
  try {
    return await fetch('/api/data');  // Awaits before returning
  } catch (err) {
    return fallback;  // Runs on fetch error
  }
}

Why it happens: return fetch() returns a pending Promise immediately, before it can reject. The catch block only catches errors that happen synchronously within the try.

The rule: Inside a try block, always return await if you want the catch to handle the rejection.

Note: Outside of try/catch, return await is redundant and ESLint's no-return-await rule will flag it. But inside try/catch, it's required.


PatternCode
Basic asyncasync function f() { return await x(); }
Parallelawait Promise.all([a(), b()])
Sequentialfor (const x of items) await process(x)
TimeoutPromise.race([fetch(url), timeout(5000)])
Cancelcontroller.abort() with signal option
Partial successPromise.allSettled([...])

  • MDN: async function
  • MDN: Promise.all
  • MDN: AbortController
  • JavaScript.info: Async/await

When to Use JavaScript Async/Await

  • Fetching data from APIs, databases, or files.
  • Running independent async work in parallel with Promise.all.
  • Handling partial failures with Promise.allSettled (don't fail-fast).
  • Racing multiple sources or implementing timeouts with Promise.race.
  • Canceling in-flight work (e.g., user navigates away) with AbortController.

Check Your Understanding: JavaScript Async/Await

Prompt

Fetch user and posts in parallel. Allow posts to fail without failing the whole request.

What a strong answer looks like

Use Promise.allSettled to avoid fail-fast, then check each result's status property. Handle rejected results separately while using fulfilled values normally.

What You'll Practice: JavaScript Async/Await

async function declarations and async arrow functionsawait expressions and Promise unwrappingPromise.all() for parallel executionPromise.allSettled() for partial-failure workflowsPromise.race() for timeouts and first-response patternsPromise.any() for first-success patternstry/catch around await for error handlingAbortController for fetch cancellationfor await...of for async iterationTop-level await in ES modules

Common JavaScript Async/Await Pitfalls

  • Forgetting to await (leaking Promise { <pending> } upward)
  • Using await outside an async function
  • Sequential awaits in loops when parallel is possible
  • Using forEach with async callbacks (forEach ignores the returned promises)
  • Promise.all fail-fast surprising you (use allSettled if partial success is OK)
  • Unhandled promise rejections (Node warns, browser logs, both can crash)
  • Swallowing errors by catching without re-throwing or surfacing
  • Forgetting that async functions always return a Promise (even when returning a value)
  • Using return instead of return await inside try/catch (error bypasses the catch block)

JavaScript Async/Await FAQ

Why am I seeing Promise { <pending> } instead of data?

You forgot to await. An async function returns a Promise, so without await you get the Promise object, not its resolved value. Add await before the function call.

Where can I use await?

Only inside an async function, or at the top level of an ES module. Using await outside these contexts causes a syntax error.

Should I await inside a loop?

Only if you need strict sequential execution. If iterations are independent, build an array of promises and use Promise.all (or allSettled) for parallelism.

Why doesn't await work with Array.forEach?

forEach returns undefined, so there's nothing to await. The async callbacks fire but forEach doesn't wait for them. Use for...of for sequential, or map + Promise.all for parallel.

Promise.all vs Promise.allSettled: which should I use?

Promise.all is fail-fast: one rejection rejects the entire result. allSettled waits for every promise and returns each outcome with status: "fulfilled" or "rejected". Use allSettled when partial success is acceptable.

How do I handle async errors properly?

Wrap await in try/catch, or attach .catch() to the promise. Watch for "unhandled rejection" warnings: they mean a rejection went uncaught.

How do I cancel a fetch request?

Create an AbortController, pass its signal to fetch, then call controller.abort(). The fetch rejects with an AbortError.

What is Promise.race useful for?

Race returns whichever promise settles first. Useful for timeouts ("fetch or timeout after 5s") or "first response wins" patterns.

What about Promise.any?

Promise.any returns the first fulfilled promise, ignoring rejections. If all reject, it throws AggregateError containing all the rejection reasons.

Why did my try/catch not catch the error?

If you used return instead of return await, the Promise was returned before it rejected. Inside try/catch, always use return await so the catch block can handle rejections.

JavaScript Async/Await Syntax Quick Reference

Basic async/await
async function fetchJson(url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
Async arrow function
const fetchData = async (id) => {
  const res = await fetch(`/api/items/${id}`);
  return res.json();
};
Parallel with Promise.all
const [user, posts] = await Promise.all([
  fetchUser(id),
  fetchPosts(id)
]);
Partial failures with Promise.allSettled
const results = await Promise.allSettled([
  fetchUser(),
  fetchPosts()
]);
const [userR, postsR] = results;
if (userR.status === "rejected") throw userR.reason;
const posts = postsR.status === "fulfilled" ? postsR.value : [];
Error handling with try/catch
try {
  const data = await fetchData();
  process(data);
} catch (err) {
  console.error("Request failed:", err);
}
Fetch cancellation with AbortController
const controller = new AbortController();
const res = fetch(url, { signal: controller.signal });

// Later: cancel the request
controller.abort();
Timeout with Promise.race
const timeout = (ms) => new Promise((_, reject) =>
  setTimeout(() => reject(new Error("Timeout")), ms)
);

const data = await Promise.race([
  fetchData(),
  timeout(5000)
]);
for...of (sequential)
for (const url of urls) {
  const data = await fetch(url);
  await processData(data);
}
map + Promise.all (parallel)
const results = await Promise.all(
  urls.map(url => fetch(url).then(r => r.json()))
);
Async iteration (for await...of)
for await (const chunk of readableStream) {
  process(chunk);
}

JavaScript Async/Await Sample Exercises

Example 1Difficulty: 1/5

Handle promise `p` error by logging the error.

p.catch(error => console.error(error));
Example 2Difficulty: 2/5

Wait for the first of `p1` or `p2` to settle (resolve/reject).

Promise.race([p1, p2])
Example 3Difficulty: 2/5

Chain `fetchData()`, then `parse`, then `save`, then catch `handleError`.

fetchData().then(parse).then(save).catch(handleError);

+ 34 more exercises

Quick Reference
JavaScript Async/Await Cheat Sheet →

Copy-ready syntax examples for quick lookup

Related Design Patterns

Observer Pattern

Also in Other Languages

Rust Async Practice: async/await, Futures & Tokio Basics

Start practicing JavaScript Async/Await

Free daily exercises with spaced repetition. No credit card required.

← Back to JavaScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.