# ⚡️ Stop Writing Flaky Tests: Your Foundational Guide to Async in Playwright

🤖 In our last article, we built a scalable **Page Object Model**, giving our tests a solid architectural blueprint. We organized our locators and actions into clean, reusable classes. But even the best blueprint can't prevent a house from collapsing if the foundation is shaky.

In test automation, that shaky foundation is often a misunderstanding of **asynchronicity**.

This article tackles that head-on. We'll explore why Playwright is inherently asynchronous and how to manage it. You will learn the three essential pillars of async mastery:

* `async/await`: The fundamental syntax for controlling the flow of your tests.
    
* `Promise.all`: The secret to speeding up test execution by running operations in parallel.
    
* `try/catch`: The safety net for building robust tests that don't crash unexpectedly.
    

Mastering these concepts is the difference between tests that are a flaky liability and an automated suite that is a truly dependable asset.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1749794533620/ccec3560-58bd-48ae-a06d-ac131ceb2bec.jpeg align="center")

---

### ✅ Prerequisites

This guide builds upon the concepts from our previous article on the Page Object Model. You should be comfortable with the following:

* TypeScript Fundamentals:
    
    * [Basic TypeScript types (`string`, `number`, `boolean`)](https://idavidov.eu/your-first-steps-in-typescript-a-practical-roadmap-for-automation-qa)
        
    * [Structuring data with `Arrays` and `Objects`](https://idavidov.eu/how-to-use-arrays-and-objects-in-typescript-for-powerful-qa-automation-scripts) 
        
    * [Writing reusable code with `Functions`](https://idavidov.eu/how-to-master-fundamental-typescript-logic-for-smarter-automation-qa)
        
    * [Automating actions with `Loops` (`for`, `while`)](https://idavidov.eu/how-to-master-fundamental-typescript-logic-for-smarter-automation-qa)
        
    * [Making decisions with `Conditionals` (`if/else`)](https://idavidov.eu/how-to-master-fundamental-typescript-logic-for-smarter-automation-qa)
        
    * [`Union` & `Literal` Types](https://idavidov.eu/a-practical-guide-to-typescript-custom-types-for-qa-automation)
        
    * [`Type Aliases` & `Interfaces`](https://idavidov.eu/a-practical-guide-to-typescript-custom-types-for-qa-automation)
        
* [A Playwright Project](https://idavidov.eu/building-playwright-framework-step-by-step-initial-setup): You should have a basic understanding of how to write and run a test.
    
* You understand the purpose of the [Page Object Model (`Class`)](https://idavidov.eu/stop-writing-brittle-tests-your-blueprint-for-a-scalable-typescript-pom).
    

---

### 🤔 The Problem: Why Are Web Tests Asynchronous?

Modern web applications are not static. When you click a "Login" button, you don't instantly see the next page. Your browser sends a request to a server, the server processes your credentials, and then sends a response back. This takes time.

This is the core of asynchronicity. You start an action, and you get back a **Promise**—a placeholder for a future result.

A **Promise** is like an order receipt from a coffee shop. You have proof you ordered, but you don't have your coffee yet. It can be in one of three states:

* `pending`: You've just ordered. The barista is making your coffee. The operation isn't finished.
    
* `fulfilled`: Success! Your name is called, and you have your coffee. The operation completed, and the Promise returns a value.
    
* `rejected`: Something went wrong. They're out of almond milk. The operation failed, and the Promise returns an error.
    

Nearly every Playwright command (`.click()`, `.fill()`, `.locator()`) returns a Promise because it interacts with this live, unpredictable web environment. Without telling our code to wait for these promises to resolve, our tests will fail.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1749794575819/2944d1c3-6ef4-4bbc-8bb6-009939fdcd94.jpeg align="center")

---

### 🛠️ The Solution, Part 1: async/await for Correctness

The `async` and `await` keywords are the fundamental tools for managing Promises.

* `async`: You add this to a function declaration (like `async ({ page }) => { ... }`) to tell JavaScript that this function will contain asynchronous operations.
    
* `await`: You place this before any command that returns a Promise. It tells your code to **pause execution** at that exact line and wait until the Promise is either fulfilled or rejected before moving to the next line.
    

#### The Failing Test (Without await)

Imagine you forget to use await. Your test code would look like this:

```typescript
// 🚨 THIS IS THE "BEFORE" - A FAILING TEST 🚨
test('A Failing test that forgets to await', async ({ page }) => {
    // We tell Playwright to click, but we don't wait for it to finish!
    page.getByRole('button', { name: 'Login' }).click();

    // The script jumps to this line INSTANTLY.
    // The next page hasn't loaded, so this locator doesn't exist yet.
    // 💥 TEST FAILS!
    await expect(page.getByText('idavidov')).toBeVisible();
});
```

This test fails because the `expect` command runs immediately after the click command is dispatched, not after the action is completed.

#### The Robust Fix (With await)

The solution is simple: await every single Playwright action.

```typescript
// ✅ THIS IS THE "AFTER" - A ROBUST TEST ✅
test('A robust test that correctly uses await', async ({ page }) => {
    // 1. await pauses the test here until the click is complete
    // and the resulting page navigation has started.
    await page.getByRole('button', { name: 'Login' }).click();

    // 2. The test only proceeds to this line AFTER the click is done.
    //    Playwright's auto-waiting will handle the rest.
    await expect(page.getByText('idavidov')).toBeVisible();
});
```

**Rule of Thumb:** If it's a Playwright command, put `await` in front of it.

---

### 🚀 The Solution, Part 2: Promise.all for Speed

Waiting is good, but waiting sequentially isn't always smart. What if you need to do two **independent** async things at once?

For example:

1. Click a "Publish Article" button (an async UI action).
    
2. Start listening for the API response to confirm the publish was successful (an async network action).
    

The Right Way: Concurrent Operations `Promise.all` solves this. It takes an array of promises and runs them all at the same time. It creates a new Promise that fulfills only when **all** the input promises are fulfilled.

```typescript
// ✅ The "right" way - running in parallel with Promise.all
const [publishActionPromise, responsePromise] = await Promise.all([
    // Operation 1: Start publishing the article
    articlePage.publishArticle(title, description, body, tags),

    // Operation 2: Start listening for the API response SIMULTANEOUSLY
    page.waitForResponse('**/api/articles/'),
]);

// Now you can work with the results after both are complete
const responseBody = await responsePromise.json();
```

By running these operations concurrently, you save you have the opportunity to perform actions that would be impossible in a sequential test, like catching an API response triggered by a UI action [(example)](https://idavidov.eu/building-playwright-framework-step-by-step-implementing-api-tests).

---

### 🛡️ The Solution, Part 3: `try/catch` for Resilience

What happens when a failure is acceptable? Sometimes an element might not be present, and that's okay. For example, a promotional popup or a cookie banner might not appear for every user on every visit.

#### The Brittle Test (Without `try/catch`)

If you write a test to dismiss a popup, it will crash if the popup isn't there.

```typescript
// 🚨 This test will CRASH if the popup doesn't appear
await page.locator('#promo-popup-close-button').click();

// The rest of the test will never run...
await expect(page.locator('.main-content')).toBeVisible();
```

#### The Resilient Fix (`try/catch`)

A `try/catch` block lets you attempt a "risky" action and handle the failure gracefully without stopping the test. [Check](https://idavidov.eu/building-playwright-framework-step-by-step-implementing-api-fixtures) the implementation of `try/catch` for API request.

```typescript
// ✅ A resilient test that won't crash
try {
    // We TRY to click the button, but with a short timeout.
    await page.locator('#promo-popup-close-button').click({ timeout: 2000 });
} catch (error) {
    // If the click fails (e.g., timeout), the code jumps here.
    // We can log it and the test continues on its merry way!
    console.log('Promotional popup was not present. Continuing test.');
}

// The test continues, whether the popup was there or not.
await expect(page.locator('.main-content')).toBeVisible();
```

#### Throwing Better Errors

Sometimes, you want the test to fail, but with a better error message. You can `catch` a generic Playwright error and `throw` a new, more descriptive one.

```typescript
try {
    await expect(page.locator('#user-welcome-message')).toBeVisible();
} catch (error) {
    // Catch the vague "TimeoutError"
    // And throw a custom error that explains the business impact.
    throw new Error(
        'CRITICAL FAILURE: User welcome message not found after login. Authentication failed.'
    );
}
```

This makes your test reports infinitely more useful, telling you why the failure matters.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1749794606045/3063d8ff-8ba5-4636-b7d2-34a98e3d41c1.jpeg align="center")

---

### 🚀 Your Mission: Refactor for Robustness

You now have the complete toolkit for `async` mastery. Go back to your own tests and look for opportunities to improve them:

1. **Audit for** `await`: Are you awaiting every single action?
    
2. **Look for Parallel Ops:** Can any of your sequential steps be run concurrently with `Promise.all` to speed things up?
    
3. **Identify Flaky Points:** Could a `try/catch` block make your test more resilient to optional elements or intermittent failures?
    

Mastering asynchronicity is non-negotiable for professional test automation. It elevates your tests from a source of frustration to a rock-solid foundation for quality.

---

> **🙏🏻 Thank you for reading!** Building robust, scalable automation frameworks is a journey best taken together. If you found this article helpful, consider joining a growing community of QA professionals 🚀 who are passionate about mastering modern testing.

> [Join the community and get the latest articles and tips by signing up for the newsletter.](https://idavidov.eu/newsletter)
