Missing await on a Playwright assertion is a silent bug: expect(page.getByText('Error')).toBeVisible() without await always passes because it evaluates a Promise object rather than the actual page state. JavaScript doesn't wait for browser operations to finish by default, so every Playwright method that touches the browser returns a Promise that must be awaited before the result is usable. This article explains the mechanic, shows what breaks when you forget await, and covers the patterns for async functions, stored values, and parallel operations.

The problem async/await solves

JavaScript is non-blocking by default. When you tell the browser to do something (navigate to a URL, click a button, load a page), JavaScript doesn't wait for it to finish before moving to the next line. It starts the operation and immediately continues.

This is a problem for tests. You need to navigate to the page before you click the button. You need to click before you check the result.

Before async/await existed, the solution was callbacks and Promises (covered briefly at the end). They work, but they produce tangled code that's hard to read and debug. async/await is the modern solution: it makes asynchronous operations look and behave like sequential ones.

The simplest explanation

await means: "wait here until this operation finishes, then continue."

``typescript

// Without await: doesn't wait, fails or behaves unexpectedly

test('broken example', async ({ page }) => {

page.goto('https://lab.becomeqa.com'); // starts navigation, continues immediately

page.getByRole('button', { name: 'Login' }).click(); // tries to click before page loaded

});

// With await: waits for each step to complete

test('correct example', async ({ page }) => {

await page.goto('https://lab.becomeqa.com'); // wait for navigation to complete

await page.getByRole('button', { name: 'Login' }).click(); // then click

});

`

The second test works because each await pauses execution until the operation completes.

What async means

async before a function means that function can use await inside it and will return a Promise: `typescript

// This function is async — it uses await

test('login test', async ({ page }) => {

await page.goto('https://lab.becomeqa.com');

// ...

});

`

In Playwright tests, all test functions are async automatically because they all involve browser operations. The Playwright test runner handles this. You just need to remember to put await before operations.

The rule: when to use await in Playwright

Use await before any Playwright method that:

  • Interacts with the browser (navigation, clicks, fills, selects)
  • Returns information from the page (text content, attribute values, counts)
  • Makes assertions (expect(...).toBeVisible())
`typescript

test('demonstrates await usage', async ({ page }) => {

// Navigation — await

await page.goto('https://lab.becomeqa.com');

// Actions — await

await page.getByRole('button', { name: 'Login' }).click();

await page.getByLabel('Username').fill('admin@becomeqa.com');

await page.getByLabel('Password').fill('testpass123');

await page.getByRole('button', { name: 'Submit' }).click();

// Assertions — await

await expect(page.getByRole('heading', { name: 'My Travel Items' })).toBeVisible();

// Getting values from page — await

const heading = await page.getByRole('heading').textContent();

console.log(heading); // 'My Travel Items'

// Checking visibility — await

const isVisible = await page.getByRole('button', { name: 'Add Item' }).isVisible();

expect(isVisible).toBe(true);

});

`

What happens if you forget await

The consequences depend on what you forgot it on:

Forgot await on an action:
`typescript

// This fills the field and immediately continues — often works accidentally

// but will fail under any load or slow network

page.getByLabel('Username').fill('admin@becomeqa.com');

await page.getByRole('button', { name: 'Submit' }).click(); // may click before fill completes

` Forgot await on an assertion: `typescript

// This is a very common mistake — the assertion evaluates to a Promise object

// which is always truthy, so the test always passes even when it should fail

expect(page.getByText('Error')).toBeVisible(); // WRONG — missing await, test always passes

await expect(page.getByText('Error')).toBeVisible(); // CORRECT

`

This last one is particularly dangerous: tests that always pass are worse than tests that always fail. TypeScript helps catch this. If you use strict mode, TypeScript will warn you when you have an unawaited Promise in an assertion.

Storing values with await

When you need to capture what Playwright returns:

`typescript

test('checking values', async ({ page }) => {

await page.goto('https://lab.becomeqa.com');

// Store text content

const title = await page.getByRole('heading').textContent();

// title is now a string: 'My Travel Items'

// Store a count

const rowCount = await page.getByRole('row').count();

// rowCount is a number: 5

// Store visibility status

const isButtonVisible = await page.getByRole('button', { name: 'Add Item' }).isVisible();

// isButtonVisible is a boolean: true or false

// Now use them

expect(title).toBe('My Travel Items');

expect(rowCount).toBeGreaterThan(0);

expect(isButtonVisible).toBe(true);

});

`

The pattern is always: const result = await page.someMethod().

Async functions outside of tests

When you write helper functions or Page Object methods that use Playwright, they need to be async too:

`typescript

// Page Object method — must be async

class LoginPage {

constructor(private page: Page) {}

async login(email: string, password: string) {

await this.page.getByRole('button', { name: 'Login' }).click();

await this.page.getByLabel('Username').fill(email);

await this.page.getByLabel('Password').fill(password);

await this.page.getByRole('button', { name: 'Submit' }).click();

}

}

// When you call an async method, you need await

test('login test', async ({ page }) => {

const loginPage = new LoginPage(page);

await loginPage.login('admin@becomeqa.com', 'testpass123'); // await the async method

});

`

The rule: if a function uses await inside, declare it async. If you call an async function, put await before the call.

Parallel operations with Promise.all

Sometimes you want two things to happen simultaneously. Promise.all runs multiple async operations in parallel and waits for all of them:

`typescript

// Wait for navigation AND a specific element to appear simultaneously

// This is faster than awaiting them sequentially

await Promise.all([

page.waitForURL('/dashboard'),

expect(page.getByRole('heading', { name: 'My Travel Items' })).toBeVisible(),

]);

`

In Playwright, the most common use is waiting for a navigation and an assertion together after clicking something. Playwright handles most of this automatically through auto-waiting, but you'll see Promise.all in older code and specific timing scenarios.

What Promises actually are (brief)

When you see something like:

`typescript

const textContent = page.getByRole('heading').textContent();

// textContent is now a Promise, not a string

`

A Promise is a placeholder for a value that doesn't exist yet, because the operation is still running. await unwraps the Promise, pausing execution until the value is ready:

`typescript

const textContent = await page.getByRole('heading').textContent();

// Now textContent is a string

`

You don't need to understand Promises deeply to write Playwright tests. The mental model "await makes it wait" is sufficient for 95% of test code. Just remember that every Playwright method returns a Promise, so you need await.

TypeScript helps you not forget

TypeScript knows which methods return Promises. If you forget await, TypeScript will warn you:

`

Type 'Promise' is not assignable to type 'string'

`

This is one reason to use TypeScript for Playwright tests: the compiler catches missing await before your tests run.

Quick reference

| What you're doing | Pattern |

|---|---|

| Navigate | await page.goto(url) |

| Click | await element.click() |

| Fill input | await element.fill('text') |

| Check visibility | await expect(element).toBeVisible() |

| Get text | const text = await element.textContent() |

| Get count | const n = await elements.count() |

| Call async function | await myAsyncFunction() |

| Run in parallel | await Promise.all([op1, op2]) |

FAQ

Do I need to understand how the JavaScript event loop works?

No, not for writing Playwright tests. "await makes it wait for the result" is sufficient. The event loop is the mechanism underneath, but you don't need to understand the mechanism to use the tool.

Why does my test pass even when I forgot await on expect?

Because expect(element).toBeVisible() without await returns a Promise object (which is truthy), and the test runner doesn't evaluate it as an assertion. The test sees no failures. This is a silent bug. TypeScript's strict mode flags it.

Can I use .then() instead of await?

Yes, .then() is the older Promise syntax. It works but produces harder-to-read code:

`typescript

// Older Promise syntax

page.goto('https://lab.becomeqa.com').then(() => {

return page.getByRole('button', { name: 'Login' }).click();

}).then(() => {

// ...

});

// Equivalent with async/await — much clearer

await page.goto('https://lab.becomeqa.com');

await page.getByRole('button', { name: 'Login' }).click();

`

Use async/await. Avoid .then() chains in new test code.

I see Promise in TypeScript. What does that mean? void means the function doesn't return a meaningful value. It just does something (like navigate or click). You still need await` on it, but you don't store the result in a variable. → See also: TypeScript for QA: Why Static Types Make Your Tests Better | Getting Started with Playwright: Your First Tests in 30 Minutes