The Test Forgot to Await the Request and Asserted Before the Response Arrived
Quick answer: When a test calls an async function without await, the assertions run before the request resolves, so they check stale or empty state. The failure often looks like a flaky test rather than a missing keyword, because the timing is nondeterministic. Every assertion that depends on a promise should run after that promise settles, and a lint rule can catch most missing awaits before they reach CI.
What the race actually looks like
Consider a test that creates a user and then checks the returned record. The helper returns a promise, but the test drops the await, so execution continues immediately.
// Buggy: the assertion runs before createUser resolves
test('creates a user with the given role', () => {
const user = createUser({ email: 'a@example.com', role: 'admin' });
expect(user.role).toBe('admin'); // user is a Promise, not the record
});
Because user is a promise, user.role is undefined. In other cases the test reads a shared array that the async call has not yet populated, and the assertion sees an empty list. The test sometimes passes on a fast machine and fails on a slow one, which is why a missing await is frequently mislabeled as flakiness.
Why it is intermittent instead of always failing
Promises in JavaScript queue callbacks as microtasks. If the awaited work is already resolved, or if the test happens to yield control before the assertion, the value may be present in time. When the work involves a real network round trip or a timer, the assertion reliably loses the race. The same code can produce a green run locally and a red one in CI solely because the environments schedule microtasks differently.
| Symptom | Missing await | Genuine flakiness |
|---|---|---|
| Asserted value | Undefined, a promise, or an empty array | Correct type, wrong timing |
| Failure pattern | Consistent once real I/O is involved | Passes on retry |
| Root cause | A promise is not settled before the assertion | Timing, ordering, or shared state |
The correct shape
Make the test callback async and await the call that the assertion depends on. If several independent calls can run together, start them first and await them as a group, which is both correct and fast.
test('creates a user with the given role', async () => {
const user = await createUser({ email: 'a@example.com', role: 'admin' });
expect(user.role).toBe('admin');
});
// Independent operations: start all, then await all
test('creates several users', async () => {
const results = await Promise.all([
createUser({ email: 'a@example.com' }),
createUser({ email: 'b@example.com' }),
]);
expect(results).toHaveLength(2);
});
Assertions on state that updates after a promise
A common variant is a component test that fires an event and immediately checks the rendered DOM, while the fetch that updates the DOM is still in flight. Waiting for the change with a proper assertion is the fix, rather than a hard-coded sleep.
test('shows the loaded profile', async () => {
server.use(rest.get('/api/me', (req, res, ctx) => res(ctx.json({ name: 'Ada' }))));
render(<Profile />);
// Wait for the async update instead of asserting synchronously.
expect(await screen.findByText('Ada')).toBeInTheDocument();
});
Using findBy queries or waitFor keeps the test aligned with the promise resolution without introducing a fixed delay that slows the suite and still races on a slow machine.
How an AI agent makes this worse
When an agent generates a batch of tests, it sometimes omits await on helper calls while remembering it on the main request, because it treats the helper as synchronous boilerplate. It may also add a setTimeout to "fix" a red test rather than tracing the promise chain. When reviewing generated tests, follow every value that ends up in an assertion back to its source and confirm a promise is awaited along the way. If the agent proposes a sleep to stabilize a test, that is a signal to look for an unresolved promise instead.
Catching it automatically
Static analysis is the most reliable defense. The TypeScript compiler flags floating promises under @typescript-eslint/no-floating-promises, and ESLint's no-async-promise-executor and related rules catch related mistakes. Enable these in CI so a missing await fails the build with a clear message rather than appearing as an intermittent runtime failure. A type-aware lint rule catches the problem even when the test happens to pass, which is exactly when you want to fix it.
Frequently asked questions
Why does my test pass locally but fail in CI if it is missing await?
Timing differences change who wins the race. The event loop, machine speed, and whether the promise is already resolved all affect whether the assertion runs before the value is set. CI's slower or differently scheduled environment exposes the race more often.
Is it ever fine to call a promise without await?
For fire-and-forget work whose result no assertion depends on, you can start a promise without awaiting, but you should still handle its rejection and make the intent explicit. Inside a test, almost every call that affects asserted state should be awaited.
How is this different from a test that needs a sleep?
A sleep guesses how long the async work will take and still races if it runs longer. Awaiting the promise or using a find-by query waits exactly until the work settles, so the test is deterministic and does not waste time on fast runs.
Can a missing await cause a false green instead of a failure?
Yes. If the assertion is weak, for example checking that a variable is truthy while a promise object is truthy, the test can pass without ever validating the real result. Pair the await fix with an assertion that checks the actual resolved value.
This is part of the The API Testing Guide — start there for the async and state-isolation bugs that make test suites unreliable.