This episode equips you with techniques for testing async code: handling promises with the resolves and rejects matchers, the async/await pattern, done-based callbacks, and controlling time with fake timers so tests are fast and deterministic.

Almost every modern application is full of asynchronous operations: fetching data from an API, reading files, waiting on timeouts, or running background jobs. If you don't know how to test async code properly, tests will often fail randomly — or worse, pass without actually testing anything.
Episode 6 teaches four core techniques: testing promises with resolves and rejects, writing tests with async/await, testing done-based callbacks, and controlling timers with fake timers. After this episode, you'll be able to write async tests that are fast, deterministic, and free of flakiness.
Jest provides two special matchers for promises: resolves and rejects. Both make assertions against promises without needing to handle then and catch manually:
function fetchData(id) {
return new Promise((resolve, reject) => {
if (id > 0) {
resolve({ id, name: "arif" });
} else {
reject(new Error("invalid id"));
}
});
}
test("resolves with data", () => {
return expect(fetchData(1)).resolves.toEqual({ id: 1, name: "arif" });
});
test("rejects with an error", () => {
return expect(fetchData(0)).rejects.toThrow("invalid id");
});expect(fetchData(1)).resolves.toEqual(...) reads as "this promise will resolve to an equal object." It's important to return the assertion with return — without it, Jest considers the test finished before the promise completes, and the test always passes.
The most readable approach is to write the test function as an async function and use await to wait for promises:
test("await directly in the test", async () => {
const data = await fetchData(2);
expect(data.name).toBe("arif");
});
test("waiting for several promises at once", async () => {
const results = await Promise.all([fetchData(1), fetchData(2)]);
expect(results).toHaveLength(2);
});With the async () => {} pattern inside test, Jest waits for the function to finish before evaluating the result. Combining Promise.all is very useful for testing multiple independent async operations at once.
To check that an async operation throws an error, you await the rejection and assert it:
test("async returns an error", async () => {
await expect(fetchData(-1)).rejects.toThrow("invalid id");
});
test("try catch to capture the error", async () => {
await expect(fetchData(-1)).rejects.toBeInstanceOf(Error);
});The await expect(...).rejects... pattern waits for the expected rejection. Without await, the rejects assertion won't be evaluated and the test will falsely pass.
Not all code uses promises. For callback-based functions, Jest provides the done parameter:
function slowProcess(input, cb) {
setTimeout(() => {
if (input) {
cb(null, `result ${input}`);
} else {
cb(new Error("empty input"));
}
}, 50);
}
test("successful callback", (done) => {
slowProcess("data", (err, result) => {
expect(err).toBeNull();
expect(result).toBe("result data");
done();
});
});
test("error callback", (done) => {
slowProcess("", (err) => {
expect(err.message).toBe("empty input");
done();
});
});The done parameter is a function that must be called after assertions complete. If done is never called, Jest fails the test due to a timeout — this protects you from callbacks that are never invoked.
Tests that wait on setTimeout for several seconds make the suite slow. Jest provides fake timers: fake timers that can be advanced instantly. With jest.useFakeTimers(), Jest replaces setTimeout, setInterval, and Date with controllable versions.
jest.useFakeTimers();
function scheduler(ms, cb) {
setTimeout(cb, ms);
}
test("scheduler runs faster", () => {
const cb = jest.fn();
scheduler(10000, cb);
expect(cb).not.toHaveBeenCalled();
jest.advanceTimersByTime(10000);
expect(cb).toHaveBeenCalledTimes(1);
});jest.advanceTimersByTime(10000) advances the fake time by 10 seconds in a single step, without actually waiting. That's how you test timeout and retry logic at full speed. Always call jest.useRealTimers() when you're done — for example in afterEach — so real timers return to normal.
Sometimes async code uses both timers and promises. Make sure the execution order is considered — run jest.advanceTimersByTime, then await the microtask with await Promise.resolve(). This pattern keeps tests deterministic even when combining timers and promises.
Episode 6 turned async tests from a source of flakiness into a reliable tool: the resolves and rejects matchers for promises, the readable async/await pattern, done for callbacks, and fake timers for controlling time.
Key takeaways:
return or await assertions that involve promises.resolves and rejects make promise assertions without then and catch.async test function waits to finish before being evaluated.done parameter; if it's not called, the test fails on timeout.advanceTimersByTime.jest.useRealTimers() after the test finishes.In the next episode, episode 7, we'll cover snapshot testing — the concept of comparing serialized output, using toMatchSnapshot(), updating snapshots safely through version control, and applying it to React components. It's one of the signature features that sets Jest apart from other frameworks.