Quick answer: A timeout is the client deciding to stop waiting, not a response from the server, so test it with fixed mock latency that lands just before and just after the threshold. Assert that the call aborts with the right error, the loading state resets, timers and in-flight work are cleaned up, retries follow a backoff schedule, and a response that arrives after the abort is ignored rather than applied twice. If you only test the fast, successful call, the slow path is still untested.

A timeout is a client decision, not a server error

When the threshold passes, the server may still be processing the request, which is why a blind retry can create a duplicate side effect. Three distinct decisions get conflated under the word timeout. A connect timeout means the TCP connection never established. A read or request timeout means the connection opened but the full response did not arrive in time. An idle timeout means the response started and then stalled, such as headers arriving with no body. Each needs its own case because the cleanup and the error the caller sees are different. Default values in an HTTP library are generic and rarely match your real latency distribution, so leaving them untouched either makes users stare at a spinner or kills slow but successful calls.

Pin the latency to the threshold

Use a local mock with a fixed, known delay rather than a real network, and set the delay to the threshold value and to one millisecond on either side. The boundary case is the one that reveals off-by-one configuration: a response that resolves at the limit should still succeed, while one a moment later should abort. Keep the delays large and stable so a busy machine cannot blur them, and run the near and far cases in the same test so the threshold is judged against both. A desktop API workspace can set mock latency directly and drive conditional overrides, so the timing is chosen in advance and the same run reproduces every time. Powerduck works this way, keeping the timeout test local and repeatable instead of dependent on live network conditions.

The timeout types worth covering

Timeout type Trigger Mock setup Expected client behavior
Connect TCP connection never opens Endpoint that refuses or drops the connection Fail fast, retry with backoff
Read/request Connected, full response too slow Fixed latency just over the threshold Abort, show error, reset loading
Idle/stream Response stalls after headers Send headers, then hold the body Abort the partial read, clean up
Late response Result arrives just after abort Latency just past the threshold Ignore the late result, no double apply

What to assert after the abort

Check the error type and message rather than only that a promise rejected, and confirm the loading flag returns to idle so the UI is not stuck. Verify the timer that drove the timeout is cleared and the in-flight request is cancelled rather than left running. For retries, assert the count and spacing against a backoff schedule and an overall deadline, and treat a non-idempotent POST carefully because an automatic retry can repeat a charge or a create. The late-response row matters most: when the answer arrives after the abort, the client must not apply it to state that already moved on, and must not process the same result twice. A 200 that lands late is still a response the client should treat as stale.

Honest limits

A local mock runs in the desktop main process and reproduces waiting time, but it does not model the full shape of a real failure. It cannot reproduce a half-open TCP connection, a network reset, packet loss, or a server that keeps processing after the client leaves, and application-level fixed delays do not exercise the operating system's real socket behavior. The mock proves the named timeout paths and the client's handling of them; it does not replace genuine network tests or a check of what the server does when the caller disconnects. Importing a schema does not deploy a backend, and a passing mock run is not evidence about production connectivity.

Frequently asked questions

What is the difference between a connect and a read timeout? A connect timeout means the connection never opened. A read timeout means it opened but the response did not finish in time, so the cleanup and error differ.

Why test a response that arrives after the timeout? The server may still answer. The client must ignore that late result instead of applying it to stale state or handling the same response twice.

Should a timed-out POST be retried automatically? Only if the operation is idempotent or carries an idempotency key. Otherwise the retry can repeat a side effect that the first request already performed.

How should I choose the threshold? Base it on the real latency distribution rather than a local round trip, set separate connect and read values, and confirm the choice against live measurements.

The bottom line

Treat timeout as client behavior and pin it with fixed mock latency around the exact threshold, covering the connect, read, idle, and late-response cases. Assert on the error, loading reset, cleanup, backoff-driven retries, and the late response that gets ignored, then verify real socket behavior and server-side handling separately. For more on this approach, see the API testing guide hub.