How do you test async code?
By FrontendPro Editorial Team Updated 9/26/2026
Answer
The rule that matters: the test runner must know when your async work has finished. Every technique below exists to tell it.
Return or await the promise
test("loads the user", async () => {
const user = await fetchUser(1);
expect(user.name).toBe("Mai");
});
Forgetting await is the classic mistake. The test function returns immediately, the runner marks it passed, and the assertion runs after the result is reported — a test that can never fail:
test("broken", () => {
fetchUser(1).then((u) => expect(u.name).toBe("wrong")); // always passes
});
Asserting a rejection
Assert it explicitly rather than hoping the catch ran:
await expect(fetchUser(-1)).rejects.toThrow("Not found");
expect.assertions(1); // or force the catch to have executed
try {
await fetchUser(-1);
} catch (e) {
expect(e.message).toBe("Not found");
}
Control time instead of waiting
Never put a real setTimeout in a test — it makes the suite slow and flaky. Use fake timers:
vi.useFakeTimers();
const fn = vi.fn();
debounce(fn, 1000)();
vi.advanceTimersByTime(1000);
expect(fn).toHaveBeenCalledOnce();
Do not hit the network
Intercept requests with MSW, or stub the client. Real requests make tests depend on someone else's uptime and on data you do not control.
Waiting for the UI
With Testing Library, use findBy* or waitFor, which poll until the condition holds or time out with a useful message:
expect(await screen.findByText("Welcome, Mai")).toBeInTheDocument();
A fixed sleep(500) is the thing to avoid: too short and it is flaky, too long and the suite crawls.
