⚡️ 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.
✅ 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
) - Structuring data with
Arrays
andObjects
- Writing reusable code with
Functions
- Automating actions with
Loops
(for
,while
) - Making decisions with
Conditionals
(if/else
) -
Union
&Literal
Types -
Type Aliases
&Interfaces
- Basic TypeScript types (
- A Playwright Project: You should have a basic understanding of how to write and run a test.
- You understand the purpose of the Page Object Model (
Class
).
???? 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.
????️ 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 (likeasync ({ 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:
// ???? 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.
// ✅ 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:
- Click a "Publish Article" button (an async UI action).
- 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.
// ✅ 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.
????️ 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.
// ???? 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.
// ✅ 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.
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.
???? 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:
-
Audit for
await
: Are you awaiting every single action? -
Look for Parallel Ops: Can any of your sequential steps be run concurrently with
Promise.all
to speed things up? -
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.
Popular Products
-
Gem's Ballet Natural Garnet Gemstone ...
$206.99$143.78 -
Soft Plush Teddy Bear Set for Valenti...
$63.99$43.78 -
Butt Lifting Body Shaper Shorts
$78.99$54.78 -
Slimming Waist Trainer & Thigh Trimmer
$57.99$39.78 -
Realistic Fake Poop Prank Toys
$24.99$16.78