Q25 of 40 · JavaScript
How do race conditions occur in async JavaScript, and how do you prevent them in tests?
JavaScriptMidjavascriptasyncrace-conditionstestingplaywrightflaky-tests
Short answer
Short answer: Race conditions occur when multiple async operations compete and their interleaved execution order is non-deterministic. In tests, unresolved Promises, missing awaits, or parallel setup steps that share state can cause intermittent failures. Fix with explicit awaiting, Promise.all for parallel setup, and isolated state per test.
Detail
JavaScript's single-threaded event loop does not prevent race conditions — it just means they happen at the micro-scheduling level (Promise resolution order) rather than at the OS thread level.
How they manifest in tests:
- A test reads state modified by a previous test's cleanup that hasn't awaited.
- Two test workers write to the same database rows in parallel without isolation.
- A
beforeEachhook fires an async call and doesn't await it — the test starts before the hook finishes. - A Playwright page navigation fires but the assertion runs before the DOM settles.
Prevention strategies:
- Always await async operations — missing
awaitin hooks is the most common cause. - Use Promise.all for independent parallel setup —
await Promise.all([setupA(), setupB()])ensures both complete before the test runs. - Isolate test data — each test gets its own user/record so parallel tests can't collide.
- Playwright auto-waiting — Playwright's built-in retry-with-timeout handles most DOM race conditions automatically.
- Avoid shared mutable state — closures in beforeEach should reinitialize, not reuse.
// EXAMPLE
// BUG: race condition — beforeEach doesn't await
beforeEach(() => {
db.clear(); // fire-and-forget: test may start before clear finishes
});
// FIX: always await
beforeEach(async () => {
await db.clear();
});
// BUG: parallel test workers share a user record
test("update user", async () => {
await db.update("user-1", { name: "Alice" });
expect(await db.get("user-1").name).toBe("Alice"); // another worker may have changed it
});
// FIX: isolate per test
test("update user", async ({ uniqueUser }) => {
await db.update(uniqueUser.id, { name: "Alice" });
expect(await db.get(uniqueUser.id).name).toBe("Alice"); // safe
});// WHAT INTERVIEWERS LOOK FOR
Understanding that JS race conditions come from async scheduling, not threads. Concrete examples in test context. Knowing Playwright's auto-waiting. Data isolation as the root fix for parallel test collisions.
// COMMON PITFALL
Assuming JavaScript's single thread makes race conditions impossible — Promises still interleave execution across microtask ticks, and parallel test workers are separate processes with a shared database.