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.
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.
- Beginner programs (1–10)
- Intermediate programs (11–20)
- Real-world QA scenario programs (21–31)
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.