What is Asynchronous JavaScript?
Asynchronous JavaScript allows code to run independently of the main thread, enabling tasks such as API calls, timers, or file reads to execute in the background.
JavaScript is single-threaded, meaning it executes one statement at a time, so asynchronous programming allows time-consuming tasks to execute without blocking other code.
The mechanisms for asynchronous behavior are:
-
Callbacks
-
Promises
-
async/await
Real-life Example
Putting water on to boil and chopping vegetables while it heats — you start the slow task, do other work, and come back when it's ready. (JavaScript doesn't run two things at literally the same moment on its one thread; it starts a slow operation and carries on until the result arrives.)
Playwright Automation Use Cases
-
Waiting for API responses asynchronously before asserting results (
const response = await page.request.get("/api/data");) -
Performing multiple independent actions without blocking the test flow
-
Handling dynamic page elements with asynchronous waits (locators and
expect(...)assertions wait automatically;page.waitForSelectoris the older, explicit way)
Using async JavaScript ensures efficient, non-blocking automation scripts.
What is a Promise?
A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
It allows handling asynchronous tasks more cleanly than callbacks.
Real-life Example
Ordering food online — you get a promise that your food will arrive (fulfilled) or fail to arrive (rejected).
Real-time Testing Example
In Playwright, waiting for an API response or file download before performing validations, using promises.
The Three States of a Promise
A Promise has three states — Pending, Fulfilled, and Rejected — representing the status of an asynchronous operation at any point.
| State | Description | Example |
|---|---|---|
| Pending | Initial state; operation not yet completed | Waiting for page navigation or an API response |
| Fulfilled | Operation completed successfully; returns a value | const data = await response.json(); |
| Rejected | Operation failed; returns an error | Network error, DNS failure or timeout (a 404 is not a rejection — see below) |
Real-life Example
-
Pending → waiting for food delivery
-
Fulfilled → food delivered successfully
-
Rejected → delivery failed
Playwright Automation Use Cases
-
API testing (wait for an API response with
await page.request.get) -
Dynamic waits (ensure elements exist before interacting)
-
Handling retries or failures cleanly using
.then()and.catch()
then, catch, and finally
-
then → executes when a Promise is fulfilled.
-
catch → executes when a Promise is rejected.
-
finally → executes regardless of fulfillment or rejection.
Real-life Example
-
then → food delivered successfully
-
catch → delivery failed
-
finally → leave feedback for the delivery experience
Real-time Testing Example
In Playwright, handling API requests:
-
Process the response with then
-
Handle errors with catch
-
Perform cleanup with finally
async and await
async marks a function to return a Promise and allows the use of await inside it.
await pauses execution of an async function until the Promise is settled (fulfilled or rejected), then returns the result.
async function loginTest() {
const response = await page.request.get("/api/user");
const data = await response.json();
await page.fill("#username", data.username);
await page.fill("#password", data.password);
await page.click("#loginButton");
}
Benefits
-
Handle multiple asynchronous operations sequentially or in a readable way.
-
Write cleaner code compared to
.then()chaining. -
Make async automation scripts maintainable.
Real-life Example
Waiting for food delivery (await) inside a kitchen task (async) without stopping other kitchen work.
Quick Summary
async declares a function that returns a Promise; await waits for the Promise to resolve before proceeding, ensuring synchronous-looking code for asynchronous operations.
Why await is Used Inside async Functions
await is used inside async functions to pause execution until a Promise is resolved or rejected, making asynchronous code behave in a synchronous, readable way.
In JavaScript, async functions return Promises; without await, the code does not pause for the Promise to resolve, which can cause problems like acting on data before it arrives or element-not-found errors when interacting too early.
Important
await can only be used inside an async function.
Using it outside one throws a syntax error:
SyntaxError: await is only valid in async function.
Synchronous vs Asynchronous Code
Synchronous code executes line by line, and each statement waits for the previous one to finish (normal function calls).
Asynchronous code executes independently, allowing other tasks to run while a long operation completes (using callbacks, promises, or async/await).
| Feature | Synchronous | Asynchronous |
|---|---|---|
| Execution | Line by line, blocking | Independent, non-blocking |
| JavaScript mechanism | Normal function calls | Callbacks, Promises, async/await |
| Playwright use | Simple sequential steps | Waiting for API responses, page loads, dynamic elements using await |
Real-life Example
-
Synchronous → waiting in a queue one by one.
-
Asynchronous → ordering food online and doing other work while it's prepared.
Error Handling with try-catch and async/await
With async/await, errors from rejected Promises are handled using a try-catch block:
-
The awaited code goes in the try block.
-
If the Promise is rejected (e.g., a network error or a failed assertion), the error is caught in the catch block.
This keeps asynchronous error handling clean and readable, and in Playwright it's used to gracefully handle failed API calls, missing elements, or navigation errors without crashing the whole test.
Try It: Four Things Every Tester Should See Once
This script starts a tiny local server (three endpoints take 300 ms each, one returns 404) and runs on Node 18+. We ran it on Node 22:
import http from 'node:http';
const server = http.createServer((req, res) => {
const delay = { '/user': 300, '/orders': 300, '/cart': 300 }[req.url] ?? 0;
setTimeout(() => {
res.writeHead(req.url === '/missing' ? 404 : 200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ path: req.url }));
}, delay);
}).listen(0);
const base = `http://localhost:${server.address().port}`;
// 1. A 404 does NOT reject the promise
const r = await fetch(`${base}/missing`);
console.log('fetch /missing → resolved with status', r.status, '| r.ok =', r.ok);
// 2. Sequential vs parallel
let t = Date.now();
for (const p of ['/user', '/orders', '/cart']) await fetch(base + p);
console.log('sequential awaits:', Date.now() - t, 'ms');
t = Date.now();
await Promise.all(['/user', '/orders', '/cart'].map(p => fetch(base + p)));
console.log('Promise.all :', Date.now() - t, 'ms');
// 3. Promise.all fails fast; allSettled reports every result
const tasks = () => [Promise.resolve('login ok'), Promise.reject(new Error('cart API down')), Promise.resolve('search ok')];
try { await Promise.all(tasks()); } catch (e) { console.log('Promise.all rejected:', e.message); }
const settled = await Promise.allSettled(tasks());
console.log('allSettled:', settled.map(s => s.status === 'fulfilled' ? s.value : 'FAILED: ' + s.reason.message));
// 4. Forgetting await: the check runs before the value exists
const getTitle = async () => { await new Promise(r => setTimeout(r, 20)); return 'Dashboard'; };
const noAwait = getTitle();
console.log('without await:', noAwait instanceof Promise ? 'a Promise object, not the title' : noAwait, '| with await:', await getTitle());
server.close();
Output:
fetch /missing → resolved with status 404 | r.ok = false
sequential awaits: 916 ms
Promise.all : 305 ms
Promise.all rejected: cart API down
allSettled: [ 'login ok', 'FAILED: cart API down', 'search ok' ]
without await: a Promise object, not the title | with await: Dashboard
- A 404 doesn't reject.
fetch— and Playwright'srequest.get()— resolve normally for any HTTP status; only network-level failures reject. Always check the status yourself (expect(response.status()).toBe(200)orexpect(response).toBeOK()), or a "successful" test can be reading an error page. - Independent calls are faster in parallel: three 300 ms requests took about 900 ms one after another, and about 300 ms with
Promise.all— useful for API test setup. Don't do this with actions on one page, which must happen in order. Promise.allfails fast on the first rejection;Promise.allSettledwaits for everything and tells you which ones failed — better for health checks across several services.- Forgetting
awaitgives you a Promise object instead of the value. In a test,expect(getTitle()).toBe('Dashboard')withoutawaitcompares a Promise with a string and fails in a confusing way — or worse, an assertion runs before the action finished.
Converting async test code between tools? See how Selenium, Cypress and Playwright handle waiting in the Selenium ⇄ Playwright ⇄ Cypress Translator — Cypress, notably, chains commands instead of using await. Related: Loops & Conditions (why forEach doesn't wait).
From Real Projects
My automation background is Java with Selenium and TestNG. JavaScript comes up more and more for testers because tools like Playwright and Cypress are built on it — and most concepts on this page have a direct Java equivalent. Learning it alongside Java makes switching between frameworks much easier. A 404 doesn't reject a fetch promise — always check the status. See also: JavaScript Variables.
📚 Official documentation: MDN: Using promises · MDN: async function
FAQs
What is asynchronous JavaScript?
Code that runs independently without blocking the single-threaded main thread, using callbacks, promises, or async/await to handle long-running tasks like API calls.
What is a Promise?
An object representing the eventual completion or failure of an asynchronous operation and its resulting value — a cleaner alternative to callbacks.
What are the states of a Promise?
-
Pending (not yet completed)
-
Fulfilled (completed successfully)
-
Rejected (failed)
What do then, catch, and finally do?
-
then runs on fulfillment.
-
catch runs on rejection.
-
finally runs regardless of the outcome.
What is the difference between async and await?
async marks a function that returns a Promise and enables await; await pauses the async function until a Promise settles, then returns its result.
Why must await be used inside an async function?
Because async functions return Promises and only they can pause on await; using await outside one throws:
SyntaxError: await is only valid in async function.
What is the difference between synchronous and asynchronous code?
Synchronous code runs line by line and blocks; asynchronous code runs independently and non-blocking, using callbacks, promises, or async/await.