DEV Community

Anirban Majumdar
Anirban Majumdar

Posted on

Demystifying Promises, Async, and Await in JavaScript/TypeScript with Playwright and Cypress

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));

Enter fullscreen mode Exit fullscreen mode

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();
});

Enter fullscreen mode Exit fullscreen mode

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');
  });
});

Enter fullscreen mode Exit fullscreen mode

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')
]);
Enter fullscreen mode Exit fullscreen mode

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
});

Enter fullscreen mode Exit fullscreen mode

Common Pitfalls & Tips

Forgetting await in Playwright

page.click('button#submit'); // ❌ Won’t wait  
await page.click('button#submit'); // ✅ Correct  

Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

Instead, use

.then():

cy.get('h1').then($el => {
  cy.log($el.text());
});


Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
onlineproxy profile image
OnlineProxy

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.

Collapse
 
bernert profile image
BernerT

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?

Collapse
 
anirseven profile image
Anirban Majumdar

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.