JavaScript Promises and Async/Await Explained for Playwright Beginners

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.

Advertisement

๐Ÿ“– 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:

StateMeaningRestaurant analogy
pendingStill workingYou ordered and got a token
fulfilled (resolved)Finished successfully with a valueYour food is ready
rejectedFailed with an errorThe 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 await inside 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')]);
MethodResolves whenUse it for
Promise.allAll succeed (rejects on the first failure)Independent tasks that must all pass
Promise.allSettledAll finish, success or failureCollect every result, e.g. checking many links
Promise.raceThe first one finishesTimeouts, “whichever comes first”
Promise.anyThe first one succeedsFallback 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

  1. Write a function delay(ms) that returns a Promise resolving after ms milliseconds, and use it with await to print three messages one second apart.
  2. Rewrite this chain with async/await and try/catch: loginApi('guest').then(r => console.log(r)).catch(e => console.log(e.message)).
  3. Use Promise.all to run three delay(1000) calls in parallel and measure the total time with Date.now().
  4. In a Playwright test on Sauce Demo, collect all product names with allTextContents() and print how many products there are.
  5. Add two products to the cart using a for...of loop, then assert the cart badge shows 2.
  6. Bonus: remove one await from 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

๐ŸŽฌ 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”

Leave a Comment