|

Day 7: JavaScript Callbacks, Promises, and Async/Await for Playwright

Compact diagram of JavaScript async evolution: callback to promise to async/await

This is Day 7 of the 21-day JS to Playwright Framework series. One lesson a day. JavaScript first. TypeScript next. Playwright after that. A production framework by Day 21.

On Day 1 we put values in boxes. On Day 4 we walked lists. On Day 5 we wrapped the walk in a function. On Day 6 we put those functions next to objects and tables. Today we wait.

A Playwright spec that cannot wait is a screenshot of a loading spinner. page.goto, locator.click, expect(locator).toBeVisible, request.get — every one of those returns a Promise. If you do not understand callback, Promise, and async/await, you will write page.waitForTimeout(5000), you will forEach an API list and wonder why the assertions finish before the responses, and you will fail the interview question that is sitting in 141_Promise_IQ.js.

I am Pramod Dutta. I teach this the same way I debug a failing pipeline: open the smallest file, print the value, then ask what Playwright would do with a wait of the same shape.

The labs are not invented. They live in my LearningPlaywrightBatch repo on main:

  • chapter_13_Callback/126_Callback.js through 132_Py_of_DON.js
  • chapter_14_Promise/133_Promise.js through 141_Promise_IQ.js
  • chapter_15_Async_Await/142_Async_Await.js through 149_API_REAL_FLAKY.js

Three filenames in those folders are spelled the way they are spelled on GitHub. I will use those names. I will not invent 130_Callback_Ex01.js, 143_Converted_Code.js, or 147_Parallel_Execution.js. The files are 130_Call_Ex01_.js, 143_Coverted_Code.js, and 147_Parrallel_Execution.js. Lab 140_Promise.race.js has a dot in the filename.

Open those files. Run them with node. Then come back here and I will map every callback, every .then, and every await to a Playwright auto-wait or an APIRequestContext call you will write for the rest of this series.

*Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.*

Compact diagram of JavaScript async evolution: callback to promise to async/await

Contents

What you will be able to do after Day 7

By the end of this post you can:

  1. Explain a callback as “a function you pass so someone else can call it later” — and see that Playwright’s test() is that pattern.
  2. Tell a sync callback (forEach) from an async callback (setTimeout) and stop awaiting inside forEach.
  3. Recognise callback hell / the Pyramid of Doom as the VWO login you would write if Playwright had no Promises.
  4. Build a Promise with resolve / reject, then consume it with .then, .catch, and .finally.
  5. Pick Promise.all (fail fast), Promise.allSettled (report everyone), and Promise.race (first settler wins) on purpose.
  6. Rewrite a .then chain as async/await and handle errors with try/catch/finally.
  7. Run independent APIRequestContext calls in parallel, and dependent UI steps in sequence.
  8. Write a bounded retry around a flaky API — and know why a commented break keeps looping after success.

That is the skill. Not the syntax. The skill is waiting for a later value without nesting yourself into a corner, failing loud when it rejects, and choosing sequential versus parallel on purpose.

The labs we are actually using

From chapter_13_Callback on main:

  • 126_Callback.js
  • 127_Sync_Callback.js
  • 128_Async_Callback.js
  • 129_Callback_hell.js
  • 130_Call_Ex01_.js (filename is spelled that way, trailing underscore)
  • 131_Callback_Return.js
  • 132_Py_of_DON.js (Pyramid of Doom)

From chapter_14_Promise on main:

  • 133_Promise.js
  • 134_Promise_API.js
  • 135_Promise_Catch.js
  • 136_Promise_Finally.js
  • 137_REAL_Promise.js
  • 138_Promise_ALL.js
  • 139_Promise_AllSettled.js
  • 140_Promise.race.js (dot in the name)
  • 141_Promise_IQ.js

From chapter_15_Async_Await on main:

  • 142_Async_Await.js
  • 143_Coverted_Code.js (filename is spelled Coverted)
  • 144_AA.js
  • 145_Try_Catch.js
  • 146_Sequential_Execution.js
  • 147_Parrallel_Execution.js (filename is spelled Parrallel)
  • 148_IQ.js
  • 149_API_REAL_FLAKY.js

Clone the repo and stay on main:

git clone https://github.com/PramodDutta/LearningPlaywrightBatch.git
cd LearningPlaywrightBatch
git checkout main

You need Node.js 18 or newer. Today we still run files with node. Playwright mapping is in the prose. We do not invent a tests/day-07.spec.ts that is not in the repo.

Why Playwright is a Promise library first

Read a spec you have already seen in a batch:

test("login", async ({ page, request }) => {
  await page.goto("https://app.vwo.com");
  await page.getByPlaceholder("Email").fill("admin@example.com");
  const response = await request.get("/health");
  await expect(page.getByText("Dashboard")).toBeVisible();
});

Four awaits. Four Promises. test itself is a function that takes a callback. { page, request } is fixture injection — an object passed into that callback. If Day 6 taught you the object, today teaches you why the function is async and why every line pauses.

Playwright auto-wait is not magic. A locator action returns a Promise that resolves when the element is actionable, or rejects when the timeout wins. APIRequestContext.get returns a Promise that resolves to an APIResponse, or rejects on a network failure. expect(locator).toBeVisible() returns a Promise that retries until the assertion passes or the expect timeout fires.

If you skip this day you will still type await. You will not know what you are awaiting. That is how people write await page.waitForTimeout(8000) and call it a wait strategy.

Part 1 — Callbacks: you pass the next function

Lab 126: a callback is just a function you hand over

Lab: chapter_13_Callback/126_Callback.js

Exact file on main:

// Callback

function placeOrder(item, callback) {
    console.log("Placing order");
    callback(); // function call
}

// Define
function print() {
    console.log("Normal Fn - Done with the order");
}

// First Way
// placeOrder("Burger", print);

// Sencond Way Anoy
placeOrder("Burger", function () {
    console.log("Anoy Fn, I am also a function wihtout name!")
});

// Third Way - Arrow Fn
placeOrder("Burger", () => {
    console.log("Arrow Fn, I am also a function wihtout name!")
});

test('has title', async ({ page }) => {

});


function test(text, callback) {
    console.log("Hi, this is test");
    callback();
}

test("Verify that the login page is working", async (page) => {
    console.log("Running TC1")
});

test('has title', async ({ page }) => {

});

Three ways to pass the same idea. Named function (print). Anonymous function. Arrow function. The comments keep the classroom spelling — Sencond, Anoy, wihtout. I will not “fix” the file in this post. I quote it.

placeOrder does not know what “done” looks like. You tell it, by passing the function it should call. That is a callback.

Now look at the bottom half. I defined a toy function test(text, callback) so you can see the shape of Playwright’s test(). Playwright’s real test is a higher-order function. You pass a title and an async function. Playwright calls that function later, with fixtures. The first test('has title', async ({ page }) => {}) in this file is the empty spec you get from the getting-started docs. The second one logs "Running TC1". The third one is empty again.

Run it:

node chapter_13_Callback/126_Callback.js

You will see the burger lines, then "Hi, this is test" twice (the two live test(...) calls after the definition), and "Running TC1" once. The empty callbacks still run. They just have no body.

Playwright mapping. Every spec you write is lab 126. test(title, async ({ page, request }) => { ... }) is placeOrder("Burger", callback) with a better name. Event handlers are the same skin: page.on("dialog", (dialog) => dialog.accept()), page.on("console", (msg) => console.log(msg.text())). You pass the function. Playwright calls it when the event fires. { page } is fixture destructuring — Day 6’s object, arriving as an argument to today’s callback.

Lab 127: a sync callback finishes before the next line

Lab: chapter_13_Callback/127_Sync_Callback.js

Exact file:

let testResults = ["PASS", "FAIL", "PASS", "SKIP"];

testResults.forEach(function (result, index) {
    console.log("Test" + index + " -> " + result);
});

// "All done" prints LAST because forEach is synchronous — it finishes all 4 iterations first, then moves on.

The comment talks about "All done". That console.log is not in the file. I will not invent it. The comment is still the lesson: forEach is synchronous. All four iterations run now. Then the engine moves to the next statement.

Test0 -> PASS
Test1 -> FAIL
Test2 -> PASS
Test3 -> SKIP

Playwright mapping. Day 4 already banned await inside forEach. Today you know why. forEach will not wait for a Promise you return from the callback. If you walk tbody tr with forEach(async (i) => { await expect(rows.nth(i)).toBeVisible(); }), the test function continues. Assertions are still in flight. Use for...of or a plain for and await each step. forEach is lab 127. It is a sync callback over a list. It is not a wait.

Same trap on APIRequestContext. This is wrong:

["/health", "/ready", "/live"].forEach(async (path) => {
  const res = await request.get(path);
  expect(res.status()).toBe(200);
});

The spec can pass before the three GETs settle. Walk the list with for (const path of paths) { const res = await request.get(path); ... }.

Lab 128: an async callback runs after the rest of the file

Lab: chapter_13_Callback/128_Async_Callback.js

Exact file:

console.log("Test 1: started");

setTimeout(function () {
    console.log("Test 2 : API response received!")
}, 2000);

console.log("Test 3: Moving to next last");

Run it.

Test 1: started
Test 3: Moving to next last
Test 2 : API response received!

Test 2 is scheduled for 2000 ms later. The file does not stop. Test 3 prints now. Two seconds later the callback fires.

This is the event loop in seven lines. setTimeout does not pause the thread. It registers a callback and returns. Node keeps going.

Playwright mapping. This is why page.goto(url) without await is a lie. You scheduled the navigation. The next line — page.getByRole("button", { name: "Login" }).click() — runs against a page that is still about:blank. Playwright’s API is a Promise, not a setTimeout, but the mistake is the same: you assumed “I called it, so it finished.” You did not wait.

The fix is not setTimeout(..., 2000) inside a spec. The fix is await page.goto(url) so the next line runs after the Promise settles. Auto-wait on the locator then waits for the button to be enabled. That is two Promises. Zero hardcoded sleeps.

page.waitForTimeout(2000) is lab 128 wearing a Playwright jacket. I treat it as a defect in this series unless you are debugging a trace and you say so in the comment.

Lab 129: callback hell is a VWO login written as nests

Lab: chapter_13_Callback/129_Callback_hell.js

Exact file:

// Real QA Scenario: E2E Login Flow app.vwo.com


function openBrowser(callback) {
    console.log("opening the browser");
    setTimeout(function () {
        console.log("Step 1 - browser starting...");
        callback();
    }, 500);
}

function goToLoginPage(callback) {
    setTimeout(function () {
        console.log("Step 2: Login page loaded");
        callback();
    }, 500);
}

function enterCredentials(callback) {
    setTimeout(function () {
        console.log("Step 3: Credentials entered");
        callback();
    }, 500);
}
function clickLogin(callback) {
    setTimeout(function () {
        console.log("Step 4: Login button clicked");
        callback();
    }, 500);
}
// THIS IS CALLBACK HELL 👇

openBrowser(function () {
    goToLoginPage(function () {
        enterCredentials(function () {
            clickLogin(function () {
                console.log("Test Complete!")
            })
        })
    })
})

The comment is the lesson. Each step takes a callback. The next step lives inside the previous callback. Four steps, four indents, one pyramid. Error handling is not even in the file. If goToLoginPage failed, you would add another callback parameter and another nest.

This is a real VWO login, delayed with setTimeout so you can feel the wait. I use app.vwo.com in the batch because students already know the screen.

Run it. After about two seconds you get the four steps and Test Complete!. Each 500 ms is a fake auto-wait.

Playwright mapping. This is what your spec would look like if Playwright’s API were callback-based: goto takes a callback, fill takes a callback, click takes a callback. Playwright did not do that. Every one of those methods returns a Promise. Lab 137 chains those Promises. Lab 143 awaits them. Hold the pyramid. We flatten it twice today.

The other Playwright face of lab 129 is the wait-for-response sandwich done wrong:

// the nest people write when they have not met Promises
page.waitForResponse((r) => r.url().includes("/login"), function () {
  page.getByRole("button", { name: "Sign in" }).click(function () {
    // now assert
  });
});

The correct pairing is Promise.all (lab 138) or const pending = page.waitForResponse(...); await click; await pending. Same two actions. No pyramid.

Lab 130: callbacks with parameters, plus another sync forEach

Lab: chapter_13_Callback/130_Call_Ex01_.js

The filename is 130_Call_Ex01_.js. Trailing underscore. That is the blob on main.

Exact file:

function greetTester(name, callback) {
    console.log("Welcome, " + name);
    callback();
}

greetTester("Dev", function () {
    console.log("Let's start testing!");
});

// Callback with Parameters


function runTest(testName, callback) {
    let status = "PASS";
    callback(testName, status);
}

runTest("Login Test", function (name, result) {
    console.log(name + " → " + result);
});

// Sync Callback — forEach
let bugs = ["UI glitch", "API timeout", "Wrong redirect"];

bugs.forEach(function (bug, i) {
    console.log("Bug #" + (i + 1) + ": " + bug);
});

console.log("Total bugs: " + bugs.length);

Three patterns in one file.

  1. greetTester calls callback() with no arguments. Same as lab 126.
  2. runTest calls callback(testName, status). The callback receives the result. This is how a reporter works.
  3. forEach again — sync. "Total bugs: 3" prints after all three bugs.

Playwright mapping. test.afterEach(async ({ page }, testInfo) => { ... }) is a callback with parameters. Playwright calls it with the fixtures and the testInfo object — title, status, error, attachments. Your custom reporter’s onTestEnd(test, result) is lab 130’s runTest. You do not call it. The runner does. page.on("response", (response) => { ... }) is the same: Playwright passes the Response into your callback.

Lab 131: a callback that returns a value, then the event loop again

Lab: chapter_13_Callback/131_Callback_Return.js

Exact file:

function calculate(a, b, operation) {
    return operation(a, b);
}

let sum = calculate(10, 5, function (x, y) {
    return x + y;
});

console.log(sum);



console.log("A: Test suite started");

setTimeout(function () {
    console.log("B: Slow API test finished");
}, 1000);

console.log("C: Fast unit test finished");

Top half: the callback is the strategy. calculate does not add. You pass the add. That is a higher-order function from Day 5, now used as a callback that returns.

sum is 15.

Bottom half: the event loop again. A, then C, then B one second later. Same shape as lab 128. I put it here so you see the order next to a return value. Returning from a sync callback is immediate. Returning from a setTimeout callback is not how you get a value out — there is no return that reaches calculate. That missing return is why Promises exist.

Playwright mapping. A custom expect matcher, a fixture factory, a page.addLocatorHandler — they all take a function and may use its return. The A/C/B print is your suite: the runner starts, a fast unit assertion finishes, a slow request.get is still in flight. If you needed B’s body in C, you must wait. You cannot return from the timeout. You need an object that represents “later.” That object is a Promise.

request.get is B. Your next expect(status).toBe(200) is C if you forgot await. Lab 131 is the interview picture of a flaky API test: the assertion ran against last week’s value because this week’s GET had not come back.

Lab 132: the Pyramid of Doom, without the VWO comments

Lab: chapter_13_Callback/132_Py_of_DON.js

Exact file:

function step1(callback) {
    console.log("Open browser");
    callback();
}

function step2(callback) {
    console.log("Navigate to page");
    callback();
}

function step3(callback) {
    console.log("Click button");
    callback();
}

function step4(callback) {
    console.log("Click button");
    callback();
}

step1(function () {
    step2(function () {
        step3(function () {
            step4(function () {
                console.log("Done!");
            });
        });
    });
});

Same pyramid as lab 129. No setTimeout. The steps are sync. The shape is still hell: the next call is an argument of the previous call. step3 and step4 both log "Click button" — that is the file. I will not rename them.

This is the last callback lab. If your login has eight steps, you get eight closing }); at the bottom. One error path per level. That is enough. We need an object that represents “later.”

Playwright mapping. A Page Object that takes a callback per method is lab 132. login(page, function () { dashboard.assertWelcome(function () { ... }) }) is a smell. Return Promises. await login(page) then await dashboard.assertWelcome(). Day 8’s classes will return those Promises. Today we learn the object they return.

Part 2 — Promises: an object for a later value

A Promise is an object. It has three states: pending, fulfilled (resolved), rejected. You create one with new Promise(function (resolve, reject) { ... }). You consume one with .then, .catch, .finally. Or you await it. Same object.

page.goto(url) returns Promise<Response | null>. locator.click() returns Promise<void>. request.get(url) returns Promise<APIResponse>. expect(locator).toHaveText("Dashboard") returns a Promise that Playwright retries. You have been holding Promises since the first spec. Today we look at them.

Lab 133: a Promise is an object you can print

Lab: chapter_14_Promise/133_Promise.js

Exact file:

let order = new Promise(function (resolve, reject) {
    let foodready = true;
    if (foodready) {
        resolve("Pizza is delivered!");
    } else {
        reject("Order Cancelled!")
    }
})

console.log(order);
// A Promise is an OBJECT. It wraps a value that will be available later.

Run it.

Promise { 'Pizza is delivered!' }

foodready is true, so the executor calls resolve. By the time console.log(order) runs, the Promise is already fulfilled. The comment is the definition I want you to memorise: a Promise is an object. It wraps a value that will be available later.

Flip foodready to false yourself and run again. You will see a rejected Promise and an unhandled-rejection warning, because this file never calls .catch. That warning is the interview. A rejected Promise you ignore becomes a failed test you cannot read.

Playwright mapping. const navigation = page.goto(url) without await gives you the same kind of object lab 133 prints. console.log(navigation) is a Promise, not a Response. The later value is the response. await unwraps it. .then unwraps it. Logging the Promise does not unwrap it. I have watched SDETs assert on the Promise object and then ask why toBe(200) failed.

Lab 134: .then runs only on resolve

Lab: chapter_14_Promise/134_Promise_API.js

Exact file:

let apiCall = new Promise(function (resolve, reject) {
    resolve({ status: 200, body: "User Data" });
});

apiCall.then(function (response) {
    console.log(response);
    console.log(response.status);
    console.log(response.body);
})

// .then() runs ONLY when the promise resolves successfully.

The resolved value is an object — { status: 200, body: "User Data" }. .then receives that object. The comment is exact: .then runs only on success.

{ status: 200, body: 'User Data' }
200
User Data

Playwright mapping. This is APIRequestContext before async/await:

await request.get("https://restful-booker.herokuapp.com/booking/1").then(async (response) => {
  expect(response.status()).toBe(200);
  const body = await response.json();
  expect(body).toHaveProperty("firstname");
});

You will not write that in a new spec. You will write const response = await request.get(...). Same Promise. Lab 134 is the skin under await. response.status() on Playwright’s APIResponse is a method, not a field — the classroom object uses a field so you can print it without a browser.

Lab 135: .catch runs only on reject

Lab: chapter_14_Promise/135_Promise_Catch.js

Exact file:

let apiCall = new Promise(function (resolve, reject) {
    // I will make call...
    reject("500 Error");
});

apiCall.then(function (data) {
    console.log("Data is success!!")
}).catch(function (error) {
    console.log(error)
});

// .catch() runs ONLY when the promise is rejected.
//  .then() is completely skipped.

reject("500 Error"). The .then body does not run. .catch prints 500 Error. The comments in the file are the rule.

Playwright mapping. A 500 from the network layer, a timeout, a closed context — those reject. A 500 HTTP status from request.get does not reject by default. Playwright’s APIRequestContext resolves with an APIResponse whose status() is 500. You assert the status. You throw if you want the Promise chain to catch. That distinction fails interviews.

const response = await request.get("/admin/report");
if (response.status() >= 500) {
  throw new Error("500 Error"); // now it is lab 135
}

Locator timeouts *do* reject. await page.getByTestId("ghost").click() after 30 seconds is a rejected Promise. .catch / try/catch is how you attach a screenshot and rethrow. Do not swallow it and call the test green.

Lab 136: .finally always runs — like afterEach

Lab: chapter_14_Promise/136_Promise_Finally.js

Exact file:

let testRun = new Promise(function (resolve, reject) {
    reject("Assertion Failed");

});

testRun.then(function (data) { // Resolve
    console.log(data);
}).catch(function (error) { // Reject
    console.log(error);
}).finally(function () { // Always Executed!
    console.log("I will be executed anyhow!!");
});

// .finally() ALWAYS runs — whether the test passed or failed. Just like afterEach() in Cypress or Playwright.

Output:

Assertion Failed
I will be executed anyhow!!

The comment in the file already names Playwright. .finally is test.afterEach. Pass or fail, you close the extra context, you detach the listener, you write the HAR, you unlock the test user.

Playwright mapping. Prefer the runner hook when the cleanup belongs to the test. Use .finally / try/finally when the cleanup belongs to this one helper:

const context = await browser.newContext();
try {
  const page = await context.newPage();
  await page.goto(url);
} finally {
  await context.close(); // lab 136
}

Fixtures already do this for page. When you open a second context for a second role, you own the finally.

Lab 137: the VWO login as a Promise chain

Lab: chapter_14_Promise/137_REAL_Promise.js

Exact file:

function openBrowser() {
    return new Promise(function (resolve) {
        resolve("Browser opened!");
    });
}

function goToLogin() {
    return new Promise(function (resolve) {
        resolve("Login page loaded");
    });
}

function enterCredentials() {
    return new Promise(function (resolve) {
        resolve("Credentials entered");
    });
}

function clickLogin() {
    return new Promise(function (resolve) {
        resolve("Logged in successfully");
    });
}

openBrowser()
    .then(function (msg) {
        console.log("Step 1", msg);
        return goToLogin();
    }).then(function (msg) {
        console.log("Step 2 :", msg);
        return enterCredentials();
    }).then(function (msg) {
        console.log("Step 3 :", msg);
        return clickLogin();
    }).then(function (msg) {
        console.log("Step 4 :", msg);
    }).catch(function (error) {
        console.log("Error:", error);
    }).finally(function () {
        console.log("Done execution!");
    });

This is lab 129 with the pyramid deleted. Each function returns a Promise. Each .then returns the next Promise. One .catch for the whole flow. One .finally at the end. Compare the indent of lab 129 with the indent of lab 137. That is the whole point of Promises.

The return inside .then is the part students skip. If you forget return goToLogin(), the next .then receives undefined and the login has already started in the background, unchained. The chain is a sequence only because you return the next Promise.

Playwright mapping. This is a Page Object before we have classes (Day 8):

openBrowser()
  .then(() => page.goto("https://app.vwo.com"))
  .then(() => page.getByPlaceholder("Email").fill(user))
  .then(() => page.getByPlaceholder("Password").fill(pass))
  .then(() => page.getByRole("button", { name: "Sign in" }).click())
  .catch((error) => { console.log("Error:", error); throw error; })
  .finally(() => { /* trace stop, context close if you own it */ });

You will rewrite this as async/await in lab 143. Learn the chain first. await is sugar on this object.

A second Playwright mapping: start the waiter *before* the click, then chain.

page.waitForResponse((r) => r.url().includes("/authenticate") && r.status() === 200)
  .then(async (response) => {
    expect(response.status()).toBe(200);
  });
await page.getByRole("button", { name: "Sign in" }).click();

Better: Promise.all in lab 138. The idea is the same — the waiter is a Promise you hold.

Lab 138: Promise.all is fail-fast parallel

Lab: chapter_14_Promise/138_Promise_ALL.js

Exact file:

let checkAuth = Promise.resolve("Auth Ok");
let checkDB = Promise.resolve("DB OK");
let checkCache = Promise.resolve("Cache OK");

Promise.all([checkAuth, checkDB, checkCache]).then(function (results) {
    console.log("All checks:", results);
})

Promise.all([
    Promise.resolve("OK"),
    Promise.reject("DB DOWN"),
    Promise.resolve("OK")
])
    .then(function (r) { console.log(r); })
    .catch(function (err) { console.log("Failed:", err); });

Two pictures.

First: three resolved checks. Promise.all fulfills with an array in the same order you passed: ['Auth Ok', 'DB OK', 'Cache OK']. Order is argument order, not finish order.

Second: one reject. Promise.all rejects with "DB DOWN". The other "OK" values are discarded. Fail fast.

Promise.resolve / Promise.reject are shortcuts for an already-settled Promise. Same as new Promise(r => r("Auth Ok")).

Playwright mapping. Health checks and independent GETs:

const [auth, db, cache] = await Promise.all([
  request.get("/auth/health"),
  request.get("/db/health"),
  request.get("/cache/health"),
]);
expect(auth.ok()).toBeTruthy();
expect(db.ok()).toBeTruthy();
expect(cache.ok()).toBeTruthy();

If DB is down, Promise.all rejects only when the GET itself rejects (network). HTTP 500 still resolves. Combine with a helper that throws on !response.ok() if you want lab 138’s second picture.

The UI pairing you will write every week:

const [response] = await Promise.all([
  page.waitForResponse((r) => r.url().includes("/authenticate")),
  page.getByRole("button", { name: "Sign in" }).click(),
]);

Start both. Click and waiter run together. That is how you avoid the race where the response already finished before you subscribed. Lab 129’s nest was the callback version of this. Lab 138 is the Promise version.

Use Promise.all when every call must succeed and you want to stop at the first reject. Use it for parallel APIRequestContext when the calls do not depend on each other.

Lab 139: Promise.allSettled is a test report

Lab: chapter_14_Promise/139_Promise_AllSettled.js

Exact file:

Promise.allSettled([
    Promise.resolve("Test A Passed!"),
    Promise.reject("Test B failed"),
    Promise.resolve("Test C passed")
]).then(function (results) {
    results.forEach(function (r, i) {
        console.log("Test " + (i + 1) + ":", r.status, "-", r.value || r.reason);
    });
})
// This is like a test report — you want results for ALL tests, not just stop at the first failure.

The comment is the product requirement. You do not want fail-fast here. You want a row for every test.

Each result is { status: "fulfilled", value } or { status: "rejected", reason }. The r.value || r.reason trick prints whichever exists.

Test 1: fulfilled - Test A Passed!
Test 2: rejected - Test B failed
Test 3: fulfilled - Test C passed

Playwright mapping. A smoke of three microservices at the start of the suite. Auth up, DB down, cache up — you still want the three lines in the report, not a single Failed: DB DOWN.

const results = await Promise.allSettled([
  request.get("/auth/health"),
  request.get("/db/health"),
  request.get("/cache/health"),
]);
for (const r of results) {
  if (r.status === "fulfilled") {
    console.log(r.value.status());
  } else {
    console.log("reason", r.reason);
  }
}

Do not use allSettled and then ignore the rejected rows. The point is to see them. expect(results.filter(r => r.status === "rejected")).toEqual([]) after you have logged them is a valid gate.

Playwright’s runner is already allSettled across tests: one failed spec does not cancel the others (unless you configured it to). Lab 139 is that idea inside one spec.

Lab 140: Promise.race — and the classroom bug in the file

Lab: chapter_14_Promise/140_Promise.race.js

Exact file. I quote it as it is:

let fastServer = new Promise(function (resolve) {
    setTimeout(function () {
        resolve("Fast 100ms")
    }), 100
});

let slowServer = new Promise(function (resolve) {
    setTimeout(function () {
        resolve("Fast 500ms")
    }), 500
});

Promise.race([fastServer, slowServer]).then(function (winner) {
    console.log("Winner:", winner);
})

Look at the setTimeout lines. The delay is outside the call.

setTimeout(function () {
    resolve("Fast 100ms")
}), 100

That is the comma operator. setTimeout is called with only the function, so the delay is 0. Then the expression evaluates to 100 and throws it away. Same for 500. Both Promises resolve on the next tick. Promise.race still picks the first settler. On your machine that is usually "Fast 100ms" because it was registered first — not because 100 ms beat 500 ms.

I will not invent a “fixed” 140_Promise.race.fixed.js. The classroom file is the lesson plus the bug. The intended picture is:

setTimeout(function () { resolve("Fast 100ms"); }, 100);
setTimeout(function () { resolve("Slow 500ms"); }, 500);

Promise.race settles with whichever Promise settles first — fulfill or reject. The others keep running. Race does not cancel them.

Playwright mapping. First healthy URL wins. Timeout versus response. Two regions, take the faster.

const winner = await Promise.race([
  request.get(process.env.API_EAST),
  request.get(process.env.API_WEST),
]);

Timeout pattern:

function timeout(ms) {
  return new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms));
}
await Promise.race([request.get("/slow-report"), timeout(3000)]);

Playwright already has timeout on request.get and on locators. Prefer the built-in. Use Promise.race when you are racing two real operations, not when you are faking a timeout that the API already gives you.

page.waitForURL versus a known navigation is another race you should not write by hand. Subscribe first, then click — or Promise.all. Racing a click against a wait is how you flake.

Lab 141: Promise interview questions from the same file

Lab: chapter_14_Promise/141_Promise_IQ.js

Most of this file is commented. I will not invent extra questions. The live code at the bottom is Promise.allSettled over three API results. The commented blocks above it are the exam paper I walk in class. Here they are, uncommented in your head, with the answer I expect.

Resolve 42. new Promise(function (resolve, reject) { resolve(42); }).then(value => ...) prints Answer: 42. .then receives the resolved value.

Reject and catch. reject("Something broke") plus .catch prints Caught: Something broke. No .then body.

Return from then. Promise.resolve(5).then(val => val * 10).then(val => ...) prints Result: 50. Returning a non-Promise from .then wraps it. The next .then sees 50.

Chain +1. Promise.resolve(1) then val + 1 twice prints 1, then 2, then 3. Three ticks. Three logs.

Throw in the middle. Promise.resolve("start").then(val => { console.log(val); throw new Error("Broke at step 2"); }).then(() => console.log("This will NOT run")).catch(err => console.log("Caught:", err.message)). Prints start, then Caught: Broke at step 2. The middle .then is skipped. That is lab 135 inside a chain.

Finally after reject. Promise.reject("Test failed").then(...).catch(...).finally(...) prints Error: Test failed then Cleanup done. Lab 136 again.

Quick resolve / reject shortcuts. Promise.resolve("Quick win") and Promise.reject("Quick loss"). Same as new Promise with an immediate resolve / reject.

all of three PASS. Promise.all([t1, t2, t3]) prints ['Login: PASS', 'Search: PASS', 'Logout: PASS'].

all with one FAIL. Promise.all hits .catch with Stopped: FAIL. Fail fast. Lab 138 picture two.

The live block:

Promise.allSettled([
    Promise.resolve("API 200"),
    Promise.reject("API 500"),
    Promise.resolve("API 201")
]).then(function (results) {
    results.forEach(function (r) {
        let val = r.status === "fulfilled" ? r.value : r.reason;
        console.log(r.status + " → " + val);
    });
});
fulfilled → API 200
rejected → API 500
fulfilled → API 201

Ternary instead of r.value || r.reason. Same report. Prefer the ternary: a fulfilled value of 0 or "" would lie with ||.

Playwright mapping. This file is the interview you will get after you say “I use Playwright.” They will not ask you for a locator. They will ask you why Promise.all stopped, why .then after throw did not run, and how you would smoke three APIs and still print the 500. Answer with labs 138, 135, and 139. Then say request.get returns a Promise of APIResponse, and HTTP 500 does not reject unless you throw.

Part 3 — async/await: the same Promise, readable as steps

async on a function means: this function always returns a Promise. await inside it means: pause this function until that Promise settles, then unwrap the value. If it rejects, throw. That throw is what try/catch catches.

Playwright’s test("name", async ({ page }) => { ... }) is this. The runner awaits the Promise your test function returns.

Lab 142: .then versus await — and the missing token

Lab: chapter_15_Async_Await/142_Async_Await.js

Exact file:

getToken()
    .then(function (token) {
        return getUser(token);
    })
    .then(function (user) {
        console.log(user);
    });


async function run() {
    let token = await getToken();
    let user = await getUser();
}

getToken and getUser are not defined in this file. There is no getToken.js in the chapter. I will not invent one. The file is a conversion sketch.

Read the two halves as a diff.

.then version: getUser(token) — the token is passed.

await version: await getUser() — the token is dropped.

That is the classroom bug. await made the code look like steps, and the argument fell on the floor. The correct conversion is:

async function run() {
    let token = await getToken();
    let user = await getUser(token);
}

Playwright mapping. This is a token-then-user flow on APIRequestContext:

const auth = await request.post("/auth/login", { data: { username, password } });
const { token } = await auth.json();
const user = await request.get("/user/me", {
  headers: { Authorization: `Bearer ${token}` },
});

Sequential on purpose. The second call needs the first body. Lab 146. If you write await request.get("/user/me") without the header, you copied lab 142’s bug into a spec.

Do not run this file with node expecting output. getToken is not defined. That is honest. The next file is the runnable conversion.

Lab 143: the login chain converted (filename: Coverted)

Lab: chapter_15_Async_Await/143_Coverted_Code.js

Filename is 143_Coverted_Code.js. I will not invent Converted.

Exact file:

function openBrowser() {
    return new Promise(function (resolve) {
        resolve("Browser opened!");
    });
}

function goToLogin() {
    return new Promise(function (resolve) {
        resolve("Login page loaded");
    });
}

function enterCredentials() {
    return new Promise(function (resolve) {
        resolve("Credentials entered");
    });
}

function clickLogin() {
    return new Promise(function (resolve) {
        resolve("Logged in successfully");
    });
}

// openBrowser()
//     .then(function (msg) {
//         console.log("Step 1", msg);
//         return goToLogin();
//     }).then(function (msg) {
//         console.log("Step 2 :", msg);
//         return enterCredentials();
//     }).then(function (msg) {
//         console.log("Step 3 :", msg);
//         return clickLogin();
//     }).then(function (msg) {
//         console.log("Step 4 :", msg);
//     }).catch(function (error) {
//         console.log("Error:", error);
//     }).finally(function () {
//         console.log("Done execution!");
//     });


async function runLoginFlow() {
    let msg1 = await openBrowser();
    console.log("Step 1:", msg1);

    let msg2 = await goToLogin();
    console.log("Step 2:", msg2);

    let msg3 = await enterCredentials();
    console.log("Step 3:", msg3);

    let msg4 = await clickLogin();
    console.log("Step 4:", msg4);

}

runLoginFlow();

The commented block is lab 137. The live block is the same four steps with await. Left-to-right, top-to-bottom. This is a Playwright spec.

There is no try/catch/finally on the live function. Lab 145 adds it. If goToLogin rejected, runLoginFlow would return a rejected Promise and Node would warn. Playwright’s runner would mark the test failed — because it awaits that Promise.

Playwright mapping. This is the VWO login you will actually type:

test("VWO login", async ({ page }) => {
  await page.goto("https://app.vwo.com");
  await page.getByPlaceholder("Email").fill(user);
  await page.getByPlaceholder("Password").fill(pass);
  await page.getByRole("button", { name: "Sign in" }).click();
  await expect(page.getByText("Dashboard")).toBeVisible();
});

Each await is one .then. Auto-wait lives inside goto, fill, click, and expect. You did not write setTimeout. You did not nest. Lab 129 became lab 143.

Lab 144: async always returns a Promise

Lab: chapter_15_Async_Await/144_AA.js

Exact file:

// Basic Async/Await

async function getTestResults() {
    return "Pass";
}

// async function ALWAYS returns a Promise
getTestResults().then(function (result) {
    console.log(result);
});


async function runTest() {
    let result = await Promise.resolve("Login test passed");
    console.log(result);

    let result2 = await Promise.resolve("Dashboard test passed");
    console.log(result2);
}

runTest();

getTestResults returns the string "Pass". Because the function is async, the caller receives a Promise. That is why .then works on it. The comment is the rule.

runTest awaits two already-resolved Promises in sequence and prints both lines.

Playwright mapping. Your helper async function login(page) { await page.goto(...); ... } returns a Promise even if you do not write return. The spec must await login(page). If you forget await, the test continues while login is still filling. Lab 128’s bug, now in a helper.

expect.poll and expect(async () => { ... }).toPass() are async functions the runner awaits. Same rule.

Lab 145: try/catch/finally is then/catch/finally

Lab: chapter_15_Async_Await/145_Try_Catch.js

Exact file:

// Error Handling — try/catch


// With Promises you use .catch().
//  With async/await you use try/catch — exactly like regular JavaScript error handling.

async function testAPI() {
    try {
        let result = await Promise.reject("503 Service Unavailable");
        console.log('Result', result);
    } catch (error) {
        console.log('Error', error);
    } finally {
        console.log("Clean up!!")
    }
}

testAPI();

// try/catch/finally maps directly to .then()/.catch()/.finally() — same logic, cleaner syntax.

Output:

Error 503 Service Unavailable
Clean up!!

Result never prints. await on a rejected Promise throws. catch holds the reason. finally runs either way.

The comments in the file are the mapping table:

Promise chainasync/await
.thenthe line after await
.catchcatch (error)
.finallyfinally { ... }

Playwright mapping. Wrap an APIRequestContext block when you want a specific message, then rethrow:

test("create booking", async ({ request }) => {
  try {
    const response = await request.post("/booking", { data: payload });
    expect(response.status()).toBe(200);
    const body = await response.json();
    expect(body.bookingid).toBeTruthy();
  } catch (error) {
    console.log("Error", error);
    throw error; // do not swallow — Playwright must fail the test
  } finally {
    // revoke token, delete temp booking if you created one and the API allows it
  }
});

Swallowing the error (lab 145 without throw) makes a red API look green. I fail students for that. catch is for context. The runner still needs the reject.

Locator failures already reject. You rarely try/catch a click unless you are writing a soft probe — “is the cookie banner here?” — and even then locator.isVisible() or a page.addLocatorHandler is cleaner.

Lab 146: sequential execution when step 2 needs step 1

Lab: chapter_15_Async_Await/146_Sequential_Execution.js

Exact file:

// When Step 2 depends on Step 1's result, you MUST run them sequentially.

// Ste1 - Step 2


function apiCall(name) {
    return new Promise(function (resolve) {

        setTimeout(function () {
            resolve(name, " 200 Ok!");
        }, 1000)

    });
}


async function sequenttialTest() {
    console.log("Starting of the Test");
    let start = Date.now();

    let r1 = await apiCall("Login");
    console.log(r1);

    let r2 = await apiCall("Dashboard");
    console.log(r2);

    let r3 = await apiCall("Report");
    console.log(r3);

    console.log("Time: ~" + (Date.now() - start) + "ms");

}

sequenttialTest();

The function name is sequenttialTest. The comment says Ste1. I keep both.

Three awaits, one after another. Each apiCall waits 1000 ms. Wall time is about 3000 ms. That is the cost of sequence.

Second classroom bug: resolve(name, " 200 Ok!"). resolve takes one argument. The string " 200 Ok!" is ignored. The file prints Login, then Dashboard, then Report — not Login 200 Ok!. Lab 147 concatenates correctly with name + ": 200 OK". I will not invent a fixed 146. I tell you what the file does.

Playwright mapping. Login → token → dashboard GET. Fill email, then fill password, then click. Navigate, then assert URL. Sequence is correct when there is a dependency.

const login = await request.post("/auth/login", { data: creds });
const { token } = await login.json();
const dashboard = await request.get("/dashboard", {
  headers: { Authorization: `Bearer ${token}` },
});
const report = await request.get("/report", {
  headers: { Authorization: `Bearer ${token}` },
});

Dashboard and report both need the token, so they wait for login. They do not need each other. Those two can become lab 147. Login stays sequential.

UI is usually sequential. You cannot click Sign in before fill. Auto-wait does not make two dependent actions parallel. It makes each action wait for the element.

Lab 147: parallel execution (filename: Parrallel)

Lab: chapter_15_Async_Await/147_Parrallel_Execution.js

Filename is 147_Parrallel_Execution.js. Function is still named sequenttialTest. That is the file.

Exact file:

// When Step 2 depends on Step 1's result, you MUST run them sequentially.

// Ste1 - Step 2


function apiCall(name) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(name + ": 200 OK");
        }, 1000);
    });
}


async function sequenttialTest() {
    console.log("Starting of the Test");
    let start = Date.now();

    let [r1, r2, r3] = await Promise.all([
        apiCall("Auth Service"),
        apiCall("User Service"),
        apiCall("Payment Service")
    ])

    console.log(r1);
    console.log(r2);
    console.log(r3);

    console.log("Time: ~" + (Date.now() - start) + "ms");

}

sequenttialTest();

The header comment was copied from 146 and is now wrong for this file. The body is parallel. Three 1000 ms calls inside Promise.all. Wall time is about 1000 ms, not 3000. resolve concatenates correctly this time.

Starting of the Test
Auth Service: 200 OK
User Service: 200 OK
Payment Service: 200 OK
Time: ~1000ms

Destructuring [r1, r2, r3] is Day 4’s array destructure on the Promise.all array. Order matches the array you passed.

Playwright mapping. Independent APIRequestContext calls. Do not serialise them out of habit.

const [auth, user, payment] = await Promise.all([
  request.get("/auth/health"),
  request.get("/users/health"),
  request.get("/payments/health"),
]);

Three services, one second of slowness each, one second on the clock. Lab 146 would have cost three.

UI example: open two pages, or wait for two responses after one click.

const [users, orders] = await Promise.all([
  page.waitForResponse((r) => r.url().includes("/users")),
  page.waitForResponse((r) => r.url().includes("/orders")),
  page.goto("/dashboard"),
]);

Promise.all can mix the navigation and the waiters. Same idea as lab 138’s click-plus-wait.

Rule I write on the board:

  • Dependsawait one, then await the next (146).
  • IndependentPromise.all (147 / 138).
  • Need every outcome, including failuresPromise.allSettled (139).
  • First settler winsPromise.race (140).

Lab 148: async/await interview file

Lab: chapter_15_Async_Await/148_IQ.js

Most of the file is commented. The live tail is a small add / main. I walk the commented paper the same way as lab 141. I do not invent questions that are not in the file.

async returns a Promise. async function sayHello() { return "Hello, QA!"; } then .then prints the string. Lab 144.

await unwraps. let status = await Promise.resolve(200) logs Status code: 200.

Three-step flow. Opened browser / Clicked login / Verified dashboard. Lab 143 in miniature.

try/catch on reject. await Promise.reject("Element not found")Test failed: Element not found. Lab 145.

try/catch/finally on a 201 body. Logs status, body, then Test complete.

Event loop: A, B, await, D, C.

console.log("A");
async function test() {
    console.log("B");
    await Promise.resolve();
    console.log("C");
}
test();
console.log("D");

Prints A, B, D, C. await yields. D runs before C. This is the interview. Playwright’s version: code after await page.goto is C. Code after you *called* page.goto without await is D — it runs too soon.

Promise.all inside async. Login / Cart / Checkout in parallel. Lab 147.

allSettled health check. Auth UP, DB DOWN, Cache UP, with checkmarks in the comment. Lab 139.

for + await over endpoints. ["/login", "/users", "/orders"] sequential. This is the correct loop. Not forEach. Day 4 plus today.

Async IIFE. (async function () { let msg = await Promise.resolve("Quick async test"); console.log(msg); })(); then console.log("Outside");. Outside can print first. The IIFE is how you await at the top level in an older Node. Playwright specs do not need it — test is already async.

The live code:

async function add(a, b) {
    return a + b;
}

async function main() {
    let result = await add(10, 20);
    console.log("Sum:", result);

    let result2 = await add(result, 30);
    console.log("Total:", result2);
}

main();
Sum: 30
Total: 60

add is sequential on purpose: result2 needs result. Even a plus-function, if it is async, returns a Promise. await it.

Playwright mapping. The A/B/D/C question is the one I ask when someone shows me a flake that “only happens in CI.” They fired page.goto and asserted the title on the next line without await. Locally the cache hid it. CI is D before C.

Lab 149: a flaky API and a retry loop that does not break

Lab: chapter_15_Async_Await/149_API_REAL_FLAKY.js

Exact file:

// Flaky Test ->  100 TC, 3 Failed, I want to rerun them 3, these are flaky, it sometimes pass, failed...
//3  I want to re run

// Retry Pattern with Async/Await - REAL QA
let attempt = 0;

function flakyAPI() {
    attempt++;
    if (attempt < 3) {
        return Promise.reject("Attempt " + attempt + ": failed");
    }
    return Promise.resolve("Attempt " + attempt + ": success!");
}



async function retryTesting(maxRetries) {
    for (let i = 1; i <= maxRetries; i++) {
        try {
            let result = await flakyAPI();
            console.log('Pass Promise!, I will exit also', result);
            // break; // you will never get out of the loop due to this
        }
        catch (error) {
            console.log('Fail Promise!', error);
        }
    }

}

retryTesting(5);

This is the real QA file. The top comment is the product problem: 100 tests, 3 flaky, rerun those 3.

flakyAPI fails until attempt is 3, then succeeds forever. retryTesting(5) loops five times. break is commented out. The comment on that line is backwards from what the code does — with break uncommented you *would* leave the loop on success. As written, after the first success you keep calling. Attempts 3, 4, and 5 all pass.

Approximate output:

Fail Promise! Attempt 1: failed
Fail Promise! Attempt 2: failed
Pass Promise!, I will exit also Attempt 3: success!
Pass Promise!, I will exit also Attempt 4: success!
Pass Promise!, I will exit also Attempt 5: success!

The comment said it will exit. The code does not exit. That is the lab. I will not invent a 149_API_REAL_FLAKY_fixed.js. In class I uncomment break and run again so you see one pass and a clean stop. I also add if (i === maxRetries) throw error in the catch so a permanently dead API fails the helper.

Playwright mapping — two layers.

Layer 1: the runner. test.describe.configure({ retries: 2 }) or retries in playwright.config. The whole spec reruns. Use this for infrastructure flakes, not for a known-bad locator. I do not want retries hiding a Day 4 off-by-one.

Layer 2: the helper. Lab 149 around APIRequestContext:

async function retryGet(request, url, maxRetries) {
  let lastError;
  for (let i = 1; i <= maxRetries; i++) {
    try {
      const response = await request.get(url);
      if (!response.ok()) throw new Error("HTTP " + response.status());
      return response;
    } catch (error) {
      lastError = error;
      console.log("Fail Promise!", error);
    }
  }
  throw lastError;
}

Bounded. Throws if every attempt failed. Returns on first ok(). That is the break the classroom file left commented.

Playwright’s expect(async () => { const r = await request.get(url); expect(r.ok()).toBeTruthy(); }).toPass({ intervals: [500, 1000], timeout: 8000 }) is the library version of the same loop. Prefer it when you are polling a health endpoint until it is up.

Do not retry a POST that creates a booking unless the API is idempotent. Three retries of POST /booking is three bookings. GET and PUT with a known id are the usual safe retries.

One table: every lab to a Playwright wait or APIRequestContext call

I promised a mapping for every file. Here they sit together.

LabJavaScript ideaPlaywright wait / APIRequestContext
126pass a function, toy test()test("name", async ({ page, request }) => {}); page.on("dialog", cb)
127sync forEachdo not await inside forEach over locator.nth or request.get
128setTimeout callback laternever waitForTimeout as a strategy; await page.goto
129callback hell, VWO loginthe nest you would write without Promises; use 137 / 143
130callback with parametersafterEach(({ page }, testInfo) => {}); page.on("response", cb)
131return from sync cb; A/C/Byou cannot return a later GET; you need a Promise
132Pyramid of DoomPage Object methods must return Promises, not take cb
133Promise is an objectpage.goto returns Promise<Response>; log it and you have not unwrapped
134.then on resolverequest.get(url).then(r => r.json()) — prefer await
135.catch on rejectlocator timeout rejects; HTTP 500 on request.get does not, unless you throw
136.finally alwaystry/finally { await context.close() }; test.afterEach
137chain the VWO steps.then Page Object; return the next Promise
138Promise.all fail-fastPromise.all([page.waitForResponse(...), click()]); parallel health GETs
139allSettled reportsmoke three services, print every row including the 500
140race (+ setTimeout bug)first healthy baseURL; prefer built-in timeout over a hand-rolled race
141IQ paperwhy all stopped; why .then after throw did not run
142then vs await, dropped tokengetUser(token) — pass the auth header into the next request.get
143converted loginthe spec you type: await goto; await fill; await click; await expect
144async returns a Promiseawait login(page) or the helper is still in flight
145try/catch/finallywrap request.post; rethrow; cleanup in finally
146sequential ~3stoken then dashboard; resolve only takes one argument in this file
147parallel ~1sPromise.all on auth/user/payment health
148IQ + A B D Cforgot await page.goto — assertion is D, navigation is C
149flaky retryretries in config, or a bounded helper with a real break / throw

Key takeaways

  • A callback is a function you pass so someone else can call it. Playwright’s test(), page.on, and afterEach are callbacks. Labs 126, 130.
  • forEach is a sync callback. It will not wait. Labs 127, 130. Walk with for...of and await.
  • setTimeout does not pause the file. A, C, then B. Labs 128, 131, 148. await is how a spec pauses.
  • Callback hell / Pyramid of Doom is a VWO login with a nest per step. Labs 129, 132. Promises flatten it.
  • A Promise is an object for a later value. resolve fulfills, reject rejects. Lab 133.
  • .then / .catch / .finally = success / failure / always. Labs 134–136. finally is afterEach.
  • Return the next Promise inside .then or the chain is a lie. Lab 137.
  • Promise.all fail-fast, allSettled report everyone, race first settler. Labs 138–140. 140_Promise.race.js does not actually delay — the 100 / 500 sit outside setTimeout.
  • async always returns a Promise. await unwraps or throws. Labs 144, 145.
  • Sequential when there is a dependency (token, then GET). Parallel when there is not (Promise.all on three health URLs). Labs 146, 147. Filenames stay sequenttialTest and Parrallel.
  • Flaky GET: bounded retry, break or return on success, throw on last failure. Lab 149. Do not retry a creating POST.
  • Playwright locators, expects, and APIRequestContext are Promises. Auto-wait is a Promise that retries. Your job is to await the right ones and to pick all / allSettled / race on purpose.

FAQ

What is the difference between a callback and a Promise in Playwright tests?

A callback is a function you pass in — test(title, fn), page.on("console", fn). A Promise is an object you get back — page.goto(), request.get(), expect(locator).toBeVisible(). Playwright uses both. The runner calls your test callback. Almost every API you call inside it returns a Promise. Await the Promise. Do not turn the spec back into lab 129 nests.

Why should I not await inside forEach?

forEach is a sync callback (lab 127). It does not wait for a Promise your callback returns. The spec continues. Assertions and request.get calls are still pending. Use for...of or a for loop and await each iteration (lab 148’s endpoint loop).

Does Playwright APIRequestContext reject on HTTP 500?

No. request.get resolves to an APIResponse. status() can be 500. The Promise rejects on transport errors, timeouts, and abort — not on an HTTP error status. If you want lab 135, throw when !response.ok(). expect(response.status()).toBe(200) is the usual assertion.

When do I use Promise.all vs allSettled vs race in a Playwright spec?

Promise.all when every call must succeed and you want to stop at the first reject — click plus waitForResponse, or three health GETs that must all be up (labs 138, 147). Promise.allSettled when you want a row for every call, including failures — a smoke report (labs 139, 141). Promise.race when the first settler wins — two baseURLs, or a hand-rolled timeout (lab 140). Prefer Playwright’s own timeout option over racing a timer.

How do I wait for an API response and click at the same time?

Create the waiter first, then fire the action, and await Promise.all both (lab 138). If you click first and then waitForResponse, the response can already be gone. That is a race, and it is not Promise.race — it is a flake.

Why does my test continue before page.goto finishes?

You did not await it. Lab 148’s A/B/D/C: the line after the call is D, the line after await is C. const p = page.goto(url) holds a Promise (lab 133). Logging p does not unwrap it.

How do I retry a flaky API call in Playwright?

Bounded loop with try/catch, return on success, throw on the last failure (lab 149 with a real break). Or expect(async () => { ... }).toPass(). Or spec-level retries in config for infrastructure flakes. Do not retry a non-idempotent POST.

What is callback hell and how does async/await fix it for a login test?

Callback hell is lab 129 / 132: each step takes a cb, the next step lives inside it. async/await (lab 143) keeps the same Promises as lab 137 but reads as four lines: await open; await goto; await fill; await click. One try/catch around the block (lab 145) replaces a cb(err) at every level.

What is Day 8 of this series?

Object-oriented JavaScript — chapter_16_OOps in the same LearningPlaywrightBatch repo. You will wrap today’s await login(page) in a class so a Page Object can own the locators and return the Promises you now know how to wait for.


<script type=”application/ld+json”> { “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “What is the difference between a callback and a Promise in Playwright tests?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “A callback is a function you pass in, such as test(title, fn) or page.on(‘console’, fn). A Promise is an object you get back from page.goto(), request.get(), or expect(locator).toBeVisible(). Playwright uses both: the runner calls your test callback, and almost every API inside it returns a Promise you should await.” } }, { “@type”: “Question”, “name”: “Why should I not await inside forEach in Playwright?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “forEach is a synchronous callback. It does not wait for a Promise your callback returns, so the spec continues while assertions and request.get calls are still pending. Use for…of or a for loop and await each iteration.” } }, { “@type”: “Question”, “name”: “Does Playwright APIRequestContext reject on HTTP 500?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “No. request.get resolves to an APIResponse whose status() can be 500. The Promise rejects on transport errors, timeouts, and abort, not on an HTTP error status. Throw when !response.ok() if you want the chain to catch, or assert expect(response.status()).toBe(200).” } }, { “@type”: “Question”, “name”: “When do I use Promise.all vs allSettled vs race in a Playwright spec?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Use Promise.all when every call must succeed and you want to stop at the first reject, such as click plus waitForResponse or three health GETs. Use Promise.allSettled when you want a row for every call including failures. Use Promise.race when the first settler wins. Prefer Playwright’s timeout option over racing a timer.” } }, { “@type”: “Question”, “name”: “How do I wait for an API response and click at the same time in Playwright?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Create the waiter first, then fire the action, and await Promise.all on both. If you click first and then waitForResponse, the response can already be gone and the test flakes.” } }, { “@type”: “Question”, “name”: “Why does my Playwright test continue before page.goto finishes?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “You did not await it. After await, the next line runs when the Promise settles. Without await, the next line runs immediately and you still hold a pending Promise. Logging that Promise does not unwrap the Response.” } }, { “@type”: “Question”, “name”: “How do I retry a flaky API call in Playwright?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Write a bounded loop with try/catch, return on success, and throw on the last failure. Or use expect(async () => { … }).toPass(). Or set spec-level retries for infrastructure flakes. Do not retry a non-idempotent POST that creates data.” } }, { “@type”: “Question”, “name”: “What is callback hell and how does async/await fix it for a login test?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Callback hell is a nest where each step takes a callback and the next step lives inside it. async/await uses the same Promises but reads as sequential lines: await goto, await fill, await click. One try/catch around the block replaces an error callback at every level.” } }, { “@type”: “Question”, “name”: “What is next after Day 7 of the JS to Playwright Framework series?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Day 8 covers object-oriented JavaScript from LearningPlaywrightBatch chapter_16_OOps: classes that wrap today’s await login(page) so a Page Object can own locators and return the Promises you now know how to wait for.” } } ] } </script>

Tomorrow — Day 8: JavaScript OOP for Page Objects

A Promise without a home is a helper you will paste twice. Tomorrow we give it a class.

Day 8 of this series takes chapter_16_OOps from the same LearningPlaywrightBatch repo. You will turn today’s await openBrowser(); await goToLogin(); await enterCredentials(); await clickLogin(); into methods on an object. That is how a VWO login becomes await loginPage.login(user, pass) and how a Restful Booker helper becomes await bookingApi.create(payload). The class returns the Promises. You already know how to await, try/catch, and Promise.all them.

Series hub (bookmark this): JavaScript → TypeScript → Playwright Advanced Framework — 21-Day Guide.

Master Playwright end to end

If you want these labs as a live classroom — with VWO waits, Restful Booker APIRequestContext, parallel health checks, and the framework we assemble on Day 21 — join Playwright Automation Mastery at The Testing Academy. Lifetime access. Real projects. A job-ready suite, not a folder of console.log.

*This is Day 7 of 21. Draft only. Not published.*

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.