JavaScript Async/Await Practice Programs with Solutions (31 Programs)

Practise JavaScript promises and async/await with 31 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

Advertisement

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Async/Await lesson first.

JavaScript Beginner Programs

1. Synchronous vs asynchronous orderBeginner

Predict the order of logs when one of them is inside setTimeout.

Show solution
console.log('1. start');
setTimeout(() => console.log('3. timeout callback'), 0);
console.log('2. end');
await new Promise(resolve => setTimeout(resolve, 10));

Output

1. start
2. end
3. timeout callback

Explanation: setTimeout callbacks run later, after the current code finishes, even with a 0 ms delay.

2. Create and resolve a promiseBeginner

Create a promise that resolves with 'done' and print the result with then.

Show solution
const task = new Promise(resolve => resolve('done'));
task.then(value => console.log('Result:', value));
await task;

Output

Result: done

Explanation: a promise represents a future value. then runs when it resolves.

3. Reject a promise and catch itBeginner

Create a promise that rejects and handle it with catch.

Show solution
const failing = new Promise((resolve, reject) => reject(new Error('Network error')));
await failing.catch(error => console.log('Caught:', error.message));

Output

Caught: Network error

Explanation: reject signals failure, and .catch handles it.

4. A sleep helperBeginner

Write sleep(ms) that returns a promise, and await it between two logs.

Show solution
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
console.log('Waiting...');
await sleep(50);
console.log('Done after 50 ms');

Output

Waiting...
Done after 50 ms

Explanation: wrapping setTimeout in a promise makes delays awaitable. In Playwright tests, prefer auto-waiting over fixed sleeps.

5. Async function returns a promiseBeginner

Show that an async function always returns a promise.

Show solution
async function getTitle() {
  return 'Playwright';
}
const result = getTitle();
console.log(result instanceof Promise);
console.log(await result);

Output

true
Playwright

Explanation: async wraps the return value in a resolved promise. Use await to read the value.

6. await inside an async functionBeginner

Await a simulated API call inside an async function.

Show solution
const fetchUser = () => new Promise(resolve => setTimeout(() => resolve({ name: 'Asha' }), 20));
async function showUser() {
  const user = await fetchUser();
  console.log('User:', user.name);
}
await showUser();

Output

User: Asha

Explanation: await pauses the async function until the promise settles, without blocking other work.

7. try/catch with awaitBeginner

Catch a rejected promise using try/catch and await.

Show solution
const login = () => Promise.reject(new Error('Invalid credentials'));
try {
  await login();
} catch (error) {
  console.log('Login failed:', error.message);
}

Output

Login failed: Invalid credentials

Explanation: with await, rejected promises throw, so normal try/catch handles them.

8. Chain then callsBeginner

Chain three then calls that transform a value step by step.

Show solution
const total = await Promise.resolve(10)
  .then(n => n * 2)
  .then(n => n + 5)
  .then(n => `Total: ${n}`);
console.log(total);

Output

Total: 25

Explanation: each then receives the previous result and returns a new value to the next step.

9. finally on a promiseBeginner

Use finally to log cleanup after success and after failure.

Show solution
await Promise.resolve('ok').then(v => console.log('Success:', v)).finally(() => console.log('cleanup 1'));
await Promise.reject(new Error('fail')).catch(e => console.log('Error:', e.message)).finally(() => console.log('cleanup 2'));

Output

Success: ok
cleanup 1
Error: fail
cleanup 2

Explanation: finally runs whether the promise resolved or rejected, just like in try/catch/finally.

10. Promise.resolve and Promise.rejectBeginner

Create already-settled promises and read their values.

Show solution
const ok = await Promise.resolve(200);
console.log('Status:', ok);
const err = await Promise.reject(new Error('timeout')).catch(e => e.message);
console.log('Error:', err);

Output

Status: 200
Error: timeout

Explanation: Promise.resolve and Promise.reject create settled promises, which is useful in tests and mocks.

JavaScript Intermediate Programs

11. Sequential vs parallel timingIntermediate

Run two 50 ms tasks one after another, then in parallel, and compare the elapsed time.

Show solution
const task = (name, ms) => new Promise(r => setTimeout(() => r(name), ms));
let start = Date.now();
await task('A', 50);
await task('B', 50);
const sequential = Date.now() - start;
start = Date.now();
await Promise.all([task('A', 50), task('B', 50)]);
const parallel = Date.now() - start;
console.log('Parallel faster:', parallel < sequential);

Output

Parallel faster: true

Explanation: awaiting one by one adds up the delays. Promise.all runs independent tasks at the same time.

12. Promise.all results orderIntermediate

Run three tasks with different delays in parallel and print the results.

Show solution
const task = (name, ms) => new Promise(r => setTimeout(() => r(name), ms));
const results = await Promise.all([task('slow', 60), task('fast', 10), task('medium', 30)]);
console.log(results);

Output

[ 'slow', 'fast', 'medium' ]

Explanation: Promise.all returns results in the order of the input array, not in completion order.

13. Promise.all fails fastIntermediate

Show that Promise.all rejects as soon as any promise rejects.

Show solution
const ok = ms => new Promise(r => setTimeout(() => r('ok'), ms));
const fail = ms => new Promise((_, reject) => setTimeout(() => reject(new Error('API 500')), ms));
try {
  await Promise.all([ok(30), fail(10), ok(20)]);
} catch (error) {
  console.log('Promise.all rejected:', error.message);
}

Output

Promise.all rejected: API 500

Explanation: one rejection rejects the whole Promise.all. Use allSettled when you need every result.

14. Promise.allSettledIntermediate

Get the status of every promise, even when some fail.

Show solution
const results = await Promise.allSettled([
  Promise.resolve('page loaded'),
  Promise.reject(new Error('image failed')),
  Promise.resolve('fonts loaded')
]);
for (const r of results) console.log(r.status, r.status === 'fulfilled' ? r.value : r.reason.message);

Output

fulfilled page loaded
rejected image failed
fulfilled fonts loaded

Explanation: allSettled never rejects. It reports fulfilled or rejected for each promise.

15. Promise.race for timeoutsIntermediate

Use Promise.race to fail a slow request after 30 ms.

Show solution
const request = new Promise(r => setTimeout(() => r('response'), 100));
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), 30));
try {
  console.log(await Promise.race([request, timeout]));
} catch (error) {
  console.log(error.message);
}

Output

Request timed out

Explanation: Promise.race settles with whichever promise settles first, which makes it a simple timeout tool.

16. Promise.anyIntermediate

Use Promise.any to get the first successful mirror, ignoring failures.

Show solution
const mirror = (name, ms, ok) => new Promise((res, rej) => setTimeout(() => (ok ? res(name) : rej(new Error(name))), ms));
const fastest = await Promise.any([mirror('mirror-a', 10, false), mirror('mirror-b', 30, true), mirror('mirror-c', 50, true)]);
console.log('First success:', fastest);

Output

First success: mirror-b

Explanation: Promise.any resolves with the first fulfilled promise and only rejects if all of them fail.

17. Convert a callback to a promiseIntermediate

Wrap a callback-style function in a promise.

Show solution
function readConfig(callback) {
  setTimeout(() => callback(null, { retries: 2 }), 10);
}
const readConfigAsync = () =>
  new Promise((resolve, reject) => readConfig((err, data) => (err ? reject(err) : resolve(data))));
const config = await readConfigAsync();
console.log('Retries:', config.retries);

Output

Retries: 2

Explanation: wrapping callbacks in promises lets old APIs work with async/await.

18. Microtasks before timersIntermediate

Compare the order of Promise.then and setTimeout callbacks.

Show solution
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
await new Promise(r => setTimeout(r, 10));

Output

sync
promise
timeout

Explanation: promise callbacks are microtasks and run before timer callbacks, even with a 0 ms delay.

19. forEach does not waitIntermediate

Show why forEach with async callbacks does not wait, and fix it with for...of.

Show solution
const delay = ms => new Promise(r => setTimeout(r, ms));
const steps = ['one', 'two'];
steps.forEach(async step => { await delay(20); console.log('forEach finished', step); });
console.log('after forEach');
await delay(50);
for (const step of steps) { await delay(5); console.log('for...of finished', step); }
console.log('after for...of');

Output

after forEach
forEach finished one
forEach finished two
for...of finished one
for...of finished two
after for...of

Explanation: forEach ignores returned promises, so the code after it runs first. for...of with await waits for each step.

20. Async iteration with map and Promise.allIntermediate

Fetch details for several IDs in parallel using map and Promise.all.

Show solution
const getProduct = id => new Promise(r => setTimeout(() => r({ id, price: id * 100 }), 10));
const ids = [1, 2, 3];
const products = await Promise.all(ids.map(getProduct));
console.log(products.map(p => `#${p.id}=${p.price}`).join(', '));

Output

#1=100, #2=200, #3=300

Explanation: map creates one promise per item, and Promise.all waits for all of them.

JavaScript Real-World QA Scenario Programs

21. Retry an async actionReal-world

Retry a flaky async call up to 3 times with a short delay between attempts.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
let calls = 0;
async function flakyApi() {
  calls++;
  if (calls < 3) throw new Error(`503 on call ${calls}`);
  return 'data';
}
async function retry(fn, attempts, delayMs) {
  for (let i = 1; i <= attempts; i++) {
    try { return await fn(); }
    catch (error) { console.log(`Attempt ${i} failed: ${error.message}`); if (i < attempts) await sleep(delayMs); }
  }
  throw new Error('All retries failed');
}
console.log('Result:', await retry(flakyApi, 3, 10));

Output

Attempt 1 failed: 503 on call 1
Attempt 2 failed: 503 on call 2
Result: data

Explanation: return await inside try makes rejected promises reach the catch, so the loop can retry.

22. Poll until a job completesReal-world

Poll a job status every 10 ms until it is 'done', with a maximum number of polls.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
const statuses = ['queued', 'running', 'running', 'done'];
let call = 0;
const getStatus = async () => statuses[Math.min(call++, statuses.length - 1)];
async function waitForDone(maxPolls) {
  for (let i = 1; i <= maxPolls; i++) {
    const status = await getStatus();
    console.log(`Poll ${i}: ${status}`);
    if (status === 'done') return true;
    await sleep(10);
  }
  return false;
}
console.log('Completed:', await waitForDone(10));

Output

Poll 1: queued
Poll 2: running
Poll 3: running
Poll 4: done
Completed: true

Explanation: polling with a limit is how tests wait for background jobs. Playwright’s expect.poll does this for you.

23. Timeout wrapperReal-world

Write withTimeout(promise, ms) that rejects when a promise takes too long.

Show solution
function withTimeout(promise, ms) {
  let timer;
  const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`Timed out after ${ms} ms`)), ms); });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
const slow = new Promise(r => setTimeout(() => r('slow result'), 80));
const fast = new Promise(r => setTimeout(() => r('fast result'), 10));
console.log(await withTimeout(fast, 50));
try { await withTimeout(slow, 30); } catch (e) { console.log(e.message); }

Output

fast result
Timed out after 30 ms

Explanation: clearing the timer in finally avoids leaving stray timers running after the race settles.

24. Run test steps sequentiallyReal-world

Run login, search and checkout steps one after another and log each step.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
const steps = [
  { name: 'login', run: async () => { await sleep(10); return 'logged in'; } },
  { name: 'search', run: async () => { await sleep(5); return '3 results'; } },
  { name: 'checkout', run: async () => { await sleep(8); return 'order placed'; } }
];
for (const step of steps) console.log(`${step.name}: ${await step.run()}`);

Output

login: logged in
search: 3 results
checkout: order placed

Explanation: dependent steps must run in order, so await each one before starting the next.

25. Parallel API setupReal-world

Create test data through three independent API calls in parallel.

Show solution
const api = (name, ms) => new Promise(r => setTimeout(() => r(`${name} created`), ms));
const [user, product, coupon] = await Promise.all([api('user', 30), api('product', 20), api('coupon', 10)]);
console.log(user);
console.log(product);
console.log(coupon);

Output

user created
product created
coupon created

Explanation: independent setup steps can run together, which makes test setup faster.

26. Collect results from flaky servicesReal-world

Check three health endpoints and report which are down, without stopping at the first failure.

Show solution
const check = (name, up, ms) => new Promise((res, rej) => setTimeout(() => (up ? res(name) : rej(new Error(name))), ms));
const results = await Promise.allSettled([check('auth', true, 10), check('search', false, 20), check('payments', true, 15)]);
const down = results.filter(r => r.status === 'rejected').map(r => r.reason.message);
console.log('Up:', results.length - down.length, '| Down:', down.join(', ') || 'none');

Output

Up: 2 | Down: search

Explanation: allSettled collects every outcome, so one failed service does not hide the others.

27. Limit concurrencyReal-world

Run 5 tasks with at most 2 running at the same time.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
let running = 0, maxRunning = 0;
async function job(id) {
  running++; maxRunning = Math.max(maxRunning, running);
  await sleep(10);
  running--;
  return id;
}
async function runWithLimit(ids, limit) {
  const results = [];
  for (let i = 0; i < ids.length; i += limit) {
    results.push(...(await Promise.all(ids.slice(i, i + limit).map(job))));
  }
  return results;
}
console.log(await runWithLimit([1, 2, 3, 4, 5], 2));
console.log('Max running at once:', maxRunning);

Output

[ 1, 2, 3, 4, 5 ]
Max running at once: 2

Explanation: processing in batches protects APIs from too many simultaneous requests, similar to limiting workers.

28. Measure an async step’s durationReal-world

Measure how long an async step takes and flag it as slow when it exceeds 40 ms.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function timed(name, fn, limitMs) {
  const start = Date.now();
  await fn();
  const ms = Date.now() - start;
  console.log(`${name}: ${ms >= limitMs ? 'SLOW' : 'ok'}`);
}
await timed('load dashboard', () => sleep(10), 40);
await timed('export report', () => sleep(60), 40);

Output

load dashboard: ok
export report: SLOW

Explanation: timing async steps helps spot performance regressions in end-to-end flows.

29. Async error in one of several stepsReal-world

Stop the flow when a step fails and report which step failed.

Show solution
const steps = [
  ['open page', async () => 'ok'],
  ['submit form', async () => { throw new Error('button disabled'); }],
  ['verify message', async () => 'ok']
];
try {
  for (const [name, run] of steps) {
    await run();
    console.log('✔', name);
  }
} catch (error) {
  console.log('✘ Flow stopped:', error.message);
}

Output

✔ open page
✘ Flow stopped: button disabled

Explanation: with await in a loop, the first rejection jumps to catch and the remaining steps are skipped.

30. Wait for a conditionReal-world

Write waitFor(condition, timeout, interval) that resolves when a condition becomes true.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function waitFor(condition, timeout = 200, interval = 10) {
  const end = Date.now() + timeout;
  while (Date.now() < end) {
    if (condition()) return true;
    await sleep(interval);
  }
  throw new Error('Condition not met in time');
}
let ready = false;
setTimeout(() => { ready = true; }, 40);
console.log('Ready:', await waitFor(() => ready));
try { await waitFor(() => false, 30); } catch (e) { console.log(e.message); }

Output

Ready: true
Condition not met in time

Explanation: this is the idea behind Playwright’s auto-waiting: check repeatedly until it passes or times out.

31. Async generator for paginated dataReal-world

Use an async generator to read pages until there are no more.

Show solution
const pages = [[1, 2], [3, 4], [5]];
async function* fetchAll() {
  for (let page = 0; page < pages.length; page++) {
    await new Promise(r => setTimeout(r, 5));
    yield* pages[page];
  }
}
const items = [];
for await (const item of fetchAll()) items.push(item);
console.log(items);

Output

[ 1, 2, 3, 4, 5 ]

Explanation: for await...of consumes async iterators, which suits paginated APIs and streams.

Keep Practising

Leave a Comment