Every Playwright test is full of async and await. In this lesson you will finally understand what they mean: synchronous vs asynchronous code, Promises, async functions, error handling and parallel execution, with Playwright examples and the mistakes that cause flaky tests.
๐ What You’ll Learn: synchronous vs asynchronous code, Promise states, then/catch/finally, async and await, try/catch with await, Promise.all, allSettled, race and any, async/await in Playwright tests, and the four most common async mistakes.
โณ Why Async Matters in Playwright
Almost everything Playwright does talks to a browser: opening a page, clicking, typing, reading text. These operations take time, from a few milliseconds to several seconds. JavaScript does not stop and wait for them by default. Instead they return a Promise, an object that says “the result will arrive later”.
That is why every Playwright test looks like this:
test('login', async ({ page }) => {
await page.goto('https://www.saucedemo.com/');
await page.getByPlaceholder('Username').fill('standard_user');
});
To write reliable tests you must understand three words: Promise, async and await. This lesson explains them from zero.
๐ Synchronous vs Asynchronous Code
Synchronous code runs line by line; each line finishes before the next starts.
console.log('1. Open browser');
console.log('2. Type username');
console.log('3. Click login');
// Output is always 1, 2, 3
Asynchronous code starts a task and moves on without waiting. setTimeout is the simplest example:
console.log('1. Start');
setTimeout(() => console.log('2. Finished after 1 second'), 1000);
console.log('3. End');
// Output: 1. Start, 3. End, 2. Finished after 1 second
Line 3 printed before line 2 because JavaScript did not wait for the timer. If browser actions behaved like this without await, your test would click the login button before the username was typed.
๐ค What Is a Promise?
A Promise represents a value that is not available yet. It is always in one of three states:
| State | Meaning | Restaurant analogy |
|---|---|---|
| pending | Still working | You ordered and got a token |
| fulfilled (resolved) | Finished successfully with a value | Your food is ready |
| rejected | Failed with an error | The kitchen ran out of the dish |
// Creating a promise (you rarely do this in tests, but it shows how it works)
function loginApi(username) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (username === 'admin') resolve({ token: 'abc123' });
else reject(new Error('Invalid user'));
}, 500);
});
}
Using a Promise with then, catch and finally
loginApi('admin')
.then(result => console.log('Token:', result.token)) // fulfilled
.catch(error => console.log('Error:', error.message)) // rejected
.finally(() => console.log('Request finished')); // always runs
This style works, but long chains of .then() become hard to read. That is why async and await were added to JavaScript.
โจ async and await
- async before a function makes it return a Promise automatically and allows
awaitinside it. - await pauses the async function until the Promise settles, then gives you its value (or throws its error).
async function runLogin() {
const result = await loginApi('admin'); // waits here
console.log('Token:', result.token); // runs after the promise is fulfilled
}
runLogin();
The code now reads top to bottom like synchronous code, but it does not block the rest of the program.
Arrow functions can be async too
const getTitle = async (page) => {
return await page.title();
};
๐ก๏ธ Handling Errors with try, catch and finally
When an awaited Promise is rejected, await throws an error. Catch it with try/catch:
async function safeLogin(user) {
try {
const result = await loginApi(user);
console.log('Logged in:', result.token);
} catch (error) {
console.log('Login failed:', error.message);
} finally {
console.log('Clean up here');
}
}
safeLogin('guest'); // Login failed: Invalid user
๐ Note: In Playwright tests you usually do not catch errors. A failed action or assertion should fail the test so you see it in the report. Use try/catch in helpers, for example for optional popups.
๐งต Running Promises in Parallel: Promise.all
await one after another runs tasks in sequence. When tasks are independent, start them together with Promise.all, which waits for all of them:
// Sequential: about 1.5 seconds in total
const a = await loginApi('admin');
const b = await loginApi('admin');
const c = await loginApi('admin');
// Parallel: about 0.5 seconds in total
const [x, y, z] = await Promise.all([loginApi('admin'), loginApi('admin'), loginApi('admin')]);
| Method | Resolves when | Use it for |
|---|---|---|
Promise.all | All succeed (rejects on the first failure) | Independent tasks that must all pass |
Promise.allSettled | All finish, success or failure | Collect every result, e.g. checking many links |
Promise.race | The first one finishes | Timeouts, “whichever comes first” |
Promise.any | The first one succeeds | Fallback sources |
๐ญ Async/Await in Playwright: Real Examples
1. Every action and assertion needs await
import { test, expect } from '@playwright/test';
test('add item to cart', async ({ page }) => {
await page.goto('https://www.saucedemo.com/');
await page.getByPlaceholder('Username').fill('standard_user');
await page.getByPlaceholder('Password').fill('secret_sauce');
await page.getByRole('button', { name: 'Login' }).click();
await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
await expect(page.locator('.shopping_cart_badge')).toHaveText('1');
});
2. Methods that return values
const title = await page.title(); // string
const price = await page.locator('.inventory_item_price').first().textContent(); // string or null
const names = await page.locator('.inventory_item_name').allTextContents(); // string[]
console.log(title, price, names.length);
3. Wait for an event and trigger it together
When an action opens a new tab or triggers a network request, start waiting before the action. Promise.all is a neat way to write this:
const [newTab] = await Promise.all([
page.waitForEvent('popup'), // start waiting
page.getByRole('link', { name: 'Open docs' }).click(), // trigger it
]);
await expect(newTab).toHaveURL(/docs/);
More patterns in Playwright waits and handling popups and tabs.
4. Async helper functions
import { Page } from '@playwright/test';
export async function login(page: Page, user: string, pass: string): Promise<void> {
await page.goto('https://www.saucedemo.com/');
await page.getByPlaceholder('Username').fill(user);
await page.getByPlaceholder('Password').fill(pass);
await page.getByRole('button', { name: 'Login' }).click();
}
// in a test
await login(page, 'standard_user', 'secret_sauce'); // don't forget await here too
๐ Common Async Mistakes (and Fixes)
Mistake 1: forgetting await
// โ The test may end before the click happens, or the next step runs too early
page.getByRole('button', { name: 'Login' }).click();
// โ
await page.getByRole('button', { name: 'Login' }).click();
Mistake 2: await inside forEach
forEach does not wait for async callbacks; all iterations start at once and the test continues immediately.
const products = ['Sauce Labs Backpack', 'Sauce Labs Bike Light'];
// โ Does not wait
products.forEach(async name => {
await page.locator('.inventory_item').filter({ hasText: name }).getByRole('button').click();
});
// โ
for...of waits for each step
for (const name of products) {
await page.locator('.inventory_item').filter({ hasText: name }).getByRole('button').click();
}
Mistake 3: await on a locator
Locators are lazy descriptions, not Promises. Await the action, not the locator.
const button = page.getByRole('button', { name: 'Login' }); // โ
no await needed
await button.click(); // โ
await the action
Mistake 4: checking a value once instead of waiting
// โ Reads the text once; fails if the page is still updating
expect(await page.locator('.title').textContent()).toBe('Products');
// โ
Web-first assertion retries until it passes
await expect(page.locator('.title')).toHaveText('Products');
๐ก Tip: Turn on the ESLint rule @typescript-eslint/no-floating-promises. It flags every Promise you forgot to await, which is the most common cause of flaky Playwright tests.
๐ Assignment
- Write a function
delay(ms)that returns a Promise resolving aftermsmilliseconds, and use it withawaitto print three messages one second apart. - Rewrite this chain with async/await and try/catch:
loginApi('guest').then(r => console.log(r)).catch(e => console.log(e.message)). - Use
Promise.allto run threedelay(1000)calls in parallel and measure the total time withDate.now(). - In a Playwright test on Sauce Demo, collect all product names with
allTextContents()and print how many products there are. - Add two products to the cart using a
for...ofloop, then assert the cart badge shows2. - Bonus: remove one
awaitfrom a passing test, run it several times, and describe what goes wrong.
โ FAQ
๐ค What is async/await in JavaScript?
async marks a function that returns a Promise, and await pauses that function until a Promise settles. Together they let you write asynchronous code that reads like normal step-by-step code.
๐ค Why do Playwright tests use async and await?
Every browser action returns a Promise because it takes time. await makes each step finish before the next one starts, so the test follows the user flow in order.
๐ค What happens if I forget await in Playwright?
The action starts but the test does not wait for it. Later steps can run too early, causing random failures, or the test can finish while actions are still running.
๐ค What is the difference between Promise.all and await in sequence?
Awaiting one by one runs tasks sequentially. Promise.all starts them together and waits for all, which is faster for independent tasks.
๐ค Do I need await before expect in Playwright?
Yes for web-first assertions on locators and pages, such as await expect(locator).toBeVisible(). Plain value assertions like expect(5).toBe(5) do not need it.
๐ฏ Next Steps
- ๐ TypeScript Variables: start Module 3
- ๐ TypeScript Functions: typed async functions and
Promise<T> - ๐ญ Playwright Waits Explained
- ๐ญ Playwright Tutorial with TypeScript: the full course
๐ฌ Watch the full Playwright tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on sqatools.in
1 thought on “JavaScript Promises and Async/Await Explained for Playwright Beginners”