Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI.
Core principle: Wait for the actual condition you care about, not a guess about how long it takes.
- Tests have arbitrary delays (
setTimeout,sleep,time.sleep()) - Tests are flaky (pass sometimes, fail under load)
- Tests timeout when run in parallel
- Waiting for async operations to complete
Do not use when testing actual timing behavior (debounce, throttle intervals). If an arbitrary timeout is genuinely needed, always document why.
// Bad: guessing at timing
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// Good: waiting for condition
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();| Scenario | Pattern |
|---|---|
| Wait for event | waitFor(() => events.find(e => e.type === 'DONE')) |
| Wait for state | waitFor(() => machine.state === 'ready') |
| Wait for count | waitFor(() => items.length >= 5) |
| Wait for file | waitFor(() => fs.existsSync(path)) |
| Complex condition | waitFor(() => obj.ready && obj.value > 10) |
Generic polling function:
async function waitFor<T>(
condition: () => T | undefined | null | false,
description: string,
timeoutMs = 5000
): Promise<T> {
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
}
}Build domain-specific helpers on top: waitForEvent, waitForEventCount, waitForEventMatch.
- Polling too fast (
setTimeout(check, 1)) wastes CPU. Poll every 10ms instead. - No timeout means looping forever if the condition is never met. Always include a timeout with a clear error message.
- Stale data from caching state before the loop. Call the getter inside the loop for fresh data each iteration.
// Tool ticks every 100ms - need 2 ticks to verify partial output
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
// 200ms = 2 ticks at 100ms intervals - documented and justifiedRequirements for justified arbitrary timeouts:
- First wait for the triggering condition
- Timeout is based on known timing (not guessing)
- Comment explains why
From debugging session (2025-10-03):
- Fixed 15 flaky tests across 3 files
- Pass rate: 60% to 100%
- Execution time: 40% faster
- No more race conditions