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



This content originally appeared on DEV Community and was authored by Anirban Majumdar

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.


This content originally appeared on DEV Community and was authored by Anirban Majumdar