Testing

When and why use await in Playwright

5 mins

Intro

Working with Playwright means working with asynchronous code. Browser actions like clicking, navigating, or reading content don’t happen instantly — they return Promises that must be awaited. Misunderstanding how await works can lead to flaky, unpredictable tests.

To get the most out of Playwright, it helps to first understand what a Promise is, why Playwright relies on them, and how await fits into reliable test execution.

What’s a Promise?

A Promise is how JavaScript handles things that take time — like talking to a browser. It’s a placeholder for a result that isn’t ready yet.

When using Playwright, most actions return Promises because they don’t happen instantly. For example, reading the page title involves asking the browser, waiting for it to respond, and then getting the result. That delay is why Playwright gives you a Promise:

const title = page.title(); // not the actual title yet — it's a Promise

To get the real value, you need to wait for the Promise to resolve:

const title = await page.title(); // now it holds the actual title

 
Info

The await keyword tells JavaScript: Wait here until this is finished.

What’s actually happening behind the scenes?

promise.ts

function getTitle() {
  return new Promise((resolve) => {
    // ask the browser for the page title...
    setTimeout(() => {
      resolve('My Page Title'); // ...and return it later
    }, 300); // pretend the browser responds in 300ms
  });
}

const title = await getTitle(); // waits until resolve() happens

That’s essentially what Playwright does internally: it returns a Promise that resolves when the browser finishes the requested task.

Why this matters in Playwright

In Playwright, nearly everything is asynchronous — clicking a button, loading a page, checking for text — all these return Promises under the hood.

If you don't await them, your test moves on before the browser is finished. That leads to:

  • Using incomplete or undefined values.
  • Skipping over failed steps without noticing.
  • Flaky tests that sometimes pass, sometimes fail.

For example:

example-error.ts

// ❌ test doesn’t wait for the page to finish loading the title
const title = page.title();
expect(title).toBe('My Page'); // title is still a Promise (not resolved)

Versus the correct way:

example-correct.ts

// ✅ waits for the browser to return the title
const title = await page.title();
expect(title).toBe('My Page');

Knowing how Promises work — and why Playwright depends on them — is the foundation for writing tests that behave consistently and catch real issues.

The golden rule of async in Playwright

In Playwright, you’re in control of the timing — unlike tools that handle waits for you automatically. This is powerful, but also unforgiving: if something returns a Promise and it’s not awaited, your test doesn’t actually wait. It just keeps going, even if the browser hasn't finished the action.

That’s where the golden rule comes in:

 
Important

If a method returns a Promise, always use await — even if the test seems to work without it.

This applies to most interactions with the page — clicks, typing, navigation, extracting content, and many assertions. Not awaiting a Promise often doesn’t cause a crash or an error. Instead, it silently continues while your test logic operates on incomplete or unresolved values.

It may work once, then break the next time. Or it might pass while skipping over bugs entirely.

That’s why being strict about await is essential:

  • It makes test execution predictable and sequential.
  • It ensures each action finishes before the next one starts.
  • It avoids subtle bugs caused by racing conditions or stale elements.

If you're unsure whether something returns a Promise — check the docs, hover in your editor, or default to being cautious and await it. You’ll save yourself hours of debugging flaky tests.

Common pitfalls

Even when await is understood in theory, small mistakes in practice can still lead to unreliable tests. These pitfalls often slip through because the test seems to work — until it doesn’t.

Forgetting await inside assertions

Some Playwright methods return Promises even inside assertions — especially when you extract values directly. This can silently fail or give confusing errors.

example.ts

// ❌ textContent() returns a Promise — this won't work
expect(page.locator('h1').textContent()).toBe('Welcome');

// ✅ wait for the value explicitly
const text = await page.locator('h1').textContent();
expect(text).toBe('Welcome');

// ✅ or use Playwright's built-in expect that handles waiting for you
await expect(page.locator('h1')).toHaveText('Welcome');

Using await expect() is preferred, since it includes auto-retries and proper waiting logic.

Awaiting non-async objects

Not everything in Playwright is a Promise. A common mistake is trying to await things like locators — which are lazy references, not async operations.

example.ts

// ❌ locator itself is not a Promise
const element = await page.locator('.my-class'); // unnecessary

// ✅ just assign the locator
const element = page.locator('.my-class');
await expect(el).toBeVisible();

Only await methods that return Promises — not objects that represent elements.

Testing tips

A few habits and tools can make working with await in Playwright smoother and less error-prone:

  1. Use await expect(...) whenever possible

    Playwright’s built-in assertions like toHaveText(), toBeVisible(), and toHaveURL() are designed to wait and retry automatically. They're more reliable than extracting values manually and asserting them yourself.

    // better than manually resolving textContent() and comparing
    await expect(page.locator('h1')).toHaveText('Welcome');
    
  2. Use TypeScript or an editor that shows return types

    If you’re unsure whether something returns a Promise, hover over it in your editor. TypeScript and most modern IDEs will tell you. If it says Promise<...>, it needs to be awaited.

  3. Enable ESLint rule no-floating-promises

    This lint rule flags any Promise that isn’t awaited or properly handled. It helps catch silent mistakes like this:

    page.click('.my-class'); // flagged because it's not awaited
    
  4. Review tests for missing await

    When something seems flaky or "skipped," it’s often a missing await. Reviewing all Promise-returning methods during test reviews can prevent time-consuming debugging later.

  5. Be cautious with helper functions

    If you create custom helpers that use async Playwright APIs, make sure the helper itself is marked async and you await it when calling it.

    helper.ts

    // good helper
    async function clickAndWait(page, selector) {
      await Promise.all([page.click(selector), page.waitForLoadState()]);
    }
    
    await clickAndWait(page, '.my-class');
    

These small practices make a big difference in test reliability — especially as the codebase grows or multiple contributors are involved.

Resources