One of the most common challenges for automation engineers using JavaScript/TypeScript is handling asynchronous code. Whether you’re writing tests in Playwright or Cypress, you’ll often deal with actions that don’t resolve instantly — such as page navigation, element interactions, or API calls.
That’s where Promises, async, and await come in. Let’s break them down with automation-specific examples.
What is a Promise?
A Promise represents the eventual completion (or failure) of an asynchronous operation. Think of it like a “contract” — “I’ll give you the data when it’s ready.”
Example: Fetching Data
const dataPromise = fetch('https://api.github.com/users');
dataPromise.then(response => response.json())
.then(data => console.log(data));
In test automation, every browser action is often a Promise.
Async & Await Simplified
async marks a function as asynchronous.
await pauses the function until the Promise is resolved.
This makes code look synchronous and much easier to read.
Using Async/Await in Playwright
Playwright heavily relies on async/await. Almost every browser interaction returns a Promise.
Example: Playwright Test with Async/Await
import { test, expect } from '@playwright/test';
test('Search on Amazon', async ({ page }) => {
await page.goto('https://www.amazon.com');
await page.fill('#twotabsearchtextbox', 'Laptop');
await page.click('input[type="submit"]');
const results = page.locator('span.a-size-medium');
await expect(results.first()).toBeVisible();
});
Notice how await makes each step sequential and readable.
Without it, you’d be chaining .then() everywhere, making tests messy.
Using Async/Await in Cypress
Cypress is slightly different — it uses built-in command chaining instead of raw async/await. Cypress commands like cy.get()
and cy.click()
are asynchronous but auto-managed.
Example: Cypress Test with Implicit Async Handling
describe('Search on Amazon', () => {
it('should search for Laptop', () => {
cy.visit('https://www.amazon.com');
cy.get('#twotabsearchtextbox').type('Laptop');
cy.get('input[type="submit"]').click();
cy.get('span.a-size-medium').first().should('be.visible');
});
});
Here, Cypress internally queues commands and resolves them without you writing await.
Mixing Promises with Playwright
Sometimes you need to handle multiple promises, e.g., waiting for API + UI events together.
Example: Waiting for Navigation and Click (Playwright)
await Promise.all([
page.waitForNavigation(),
page.click('text=Login')
]);
This ensures the test doesn’t miss the navigation event.
Mixing Promises with Cypress
Cypress doesn’t expose Promises directly, but you can wrap them:
cy.wrap(Promise.resolve(42)).then(value => {
cy.log('Resolved value:', value); // 42
});
Common Pitfalls & Tips
Forgetting await in Playwright
page.click('button#submit'); // ❌ Won’t wait
await page.click('button#submit'); // ✅ Correct
Using async/await incorrectly in Cypress
Cypress commands don’t return raw Promises, so don’t do:
const text = await cy.get('h1').text(); // ❌ Not supported
Instead, use
.then():
cy.get('h1').then($el => {
cy.log($el.text());
});
Parallelization with Promises
For Playwright, prefer Promise.all when waiting for multiple things.
Conclusion
Understanding Promises, async, and await is essential for writing clean, reliable automation in JavaScript/TypeScript.
In Playwright, always use async/await to control flow.
In Cypress, commands are asynchronous but automatically queued.
For advanced scenarios, leverage Promise.all (Playwright) or cy.wrap (Cypress).
Mastering these concepts makes your automation more readable, stable, and maintainable.
Top comments (3)
Playwright and Cypress handle async stuff pretty differently, and that can totally change your coding vibe. With Playwright, you're rocking native async/await, which gives you full control over what runs when - it's like having the steering wheel yourself. Meanwhile, Cypress goes for a “we got this for you” approach: it uses command chaining and a queue system that hides away the Promises under the hood. One big trap with both tools? Forgetting await, mixing .then() with await, or not catching promise rejections properly - classic rookie mistakes. Playwright flexes hard with Promise.all() too. That means you can fire off navigation, API waits, and UI actions at the same time - real power moves. Cypress not so much. Its queueing system doesn't always vibe well with outside async code unless you manage it just right. So Playwright gives you raw power and control - like a manual transmission. Cypress trades some of that control for a smoother ride with built-in retries and simpler syntax. Depends if you wanna drive or just cruise.
Great breakdown of Playwright's explicit async/await vs Cypress's command queue. Feels like a wider trend: test tooling is moving toward async-by-default to mirror event-driven apps and microservices. Curious—do you think patterns like Promise.allSettled and AbortController will become standard in test suites as teams push more parallel API+UI workflows in CI?
Great observation 🙌. I do think we’ll see more async-by-default patterns creeping into test frameworks, mainly because modern systems (event-driven UIs, streaming APIs, microservices) don’t operate in neat sequential flows anymore.
Promise.allSettled (or similar combinators) is very relevant when you want to orchestrate multiple async tasks that may not all succeed — for example, firing several API requests in parallel while a UI action is happening, then collecting results without letting a single failure derail the test. This aligns well with resilient CI pipelines where partial success is still informative.
AbortController also feels like it’s headed into the testing mainstream. In long-running workflows, especially with Playwright’s and Cypress’s retries/timeouts, having the ability to explicitly cancel async operations (like API polling or websocket listeners) gives tighter control and avoids dangling promises that slow down suites.