Timeouts — Mastering Network Resilience and Responsiveness
In this article, we delve into the significance of timeout strategies in frontend networking, exploring how to efficiently cancel slow requests, provide early error feedback, and construct more robust and responsive web applications.
Timeouts — Mastering Network Resilience and Responsiveness
In the realm of frontend engineering, one stark reality that developers encounter is that not all network requests will complete successfully. Some requests get stuck midway, some experience significant delays, and others may disappear into the cyber void.
This is where the role of timeouts becomes crucially important.
A timeout sets a predetermined limit for a request to complete. If the request fails to finish within the stipulated time, it is either cancelled or treated as a failure. This simple yet powerful pattern safeguards your application from hanging indefinitely and provides your users with timely feedback when something goes amiss.
In this detailed article, we will delve into the following aspects:
- The essentiality of timeouts
- The implementation of timeouts in
fetch
, Axios, and other libraries - The determination of deadlines and their length
- The enhancement of resilience, UX, and observability through timeouts
- Real-world timeout behavior as demonstrated by applications like Gmail, Slack, and more
Understanding Timeouts
A timeout essentially denotes a maximum duration for a network operation to be successful. If the operation exceeds this duration, it is terminated.
Several reasons can cause a request to hang:
- Server is overloaded or unresponsive
- Network conditions are degraded
- DNS or TLS negotiation fails
- Interference by Middleware (e.g., proxies, firewalls)
Without a timeout, a request may never resolve, leading to frozen UIs, memory leaks, and a poor user experience (UX).
Implementing Timeouts in Fetch
Contrary to what one might expect, native fetch()
does not include a built-in timeout mechanism. However, you can utilize AbortController
to achieve the same:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
fetch("/api/data", { signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name === "AbortError") {
console.log("Request timed out");
}
})
.finally(() => clearTimeout(timeout));
In this code snippet, the request is cancelled if it takes more than 5 seconds. The AbortController
object provides a signal
property that can be passed to the fetch API to link it with the abort action. If the request takes more than the set time, the abort
method is called, and the fetch operation is cancelled.
Leveraging Axios for Timeout Support
Axios, a popular HTTP client, provides built-in timeout configuration:
axios.get("/api/data", { timeout: 5000 })
.then(handleResponse)
.catch((error) => {
if (error.code === "ECONNABORTED") {
console.error("Request timed out");
}
});
This snippet of code demonstrates how Axios simplifies timeout handling. The configuration object passed into the Axios request method includes a timeout
field, which specifies the limit in milliseconds. If the request takes longer, Axios automatically aborts it and throws an error with code "ECONNABORTED".
Determining When to Set Timeouts
The decision of when and how long to set timeouts should be governed by the nature and criticality of tasks:
- For UX-critical tasks such as button clicks and saves, timeouts should be short, around 3–5s.
- For content fetching tasks like loading dashboards, medium timeouts of 5–10s are appropriate.
- For background syncing tasks or retries, longer timeouts of 15–30s can be applied.
In the process of setting timeouts, always balance user expectations, network variability, backend reliability, and the difference between mobile and desktop users.
UI Patterns for Providing Timeout Feedback
- Use spinners with fallback text such as "Taking longer than expected…” to keep the user informed.
- Implement auto-dismiss or retry mechanisms on timeout.
- Show actionable errors like "Couldn’t connect — Try again".
- Use skeleton loaders to maintain layout continuity during data fetching.
Implementing Timeout with Promises
You can create a timeout wrapper around any async function:
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
);
return Promise.race([promise, timeout]);
}
withTimeout(fetch("/api/data"), 5000)
.then(res => res.json())
.catch(console.error);
In the above code, Promise.race
is used to race the fetch request against a timer promise. The promise that settles first wins the race and determines the outcome. If the fetch request is slower than the timer, the timeout error is thrown.
Combining Timeout and Retry Mechanisms
Integrating timeouts with retries can significantly enhance system resilience:
async function fetchWithRetry(url, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
return await withTimeout(fetch(url), 5000);
} catch (e) {
if (i === attempts - 1) throw e;
}
}
}
This function tries to fetch the requested URL up to the specified number of attempts. If the fetch operation fails due to a timeout, it retries the request. If all attempts fail, it throws the error.
Real-World Timeout Behavior
Gmail
- Employs aggressive timeouts for inbox syncing.
- Presents retry dialogs if fetch operations encounter delays.
Slack
- Marks channels as “disconnected” if a request stalls for more than 5s.
- Uses background retries to reestablish connection.
- Displays “We’re having trouble loading this right now” as a timeout fallback.
- Often retries data fetches silently.
Enhancing Observability and Debugging
To track timeout stats:
- Measure the percentage of total requests that time out.
- Track the average time to timeout.
- Identify the endpoints that are most affected.
- Measure the success rate of retries.
Use logging and error reporting tools like Sentry and DataDog to alert on spikes in timeouts, which often indicate backend issues or ISP degradation.
Avoiding Timeout Anti-Patterns
- Avoid setting no timeout on critical network operations (e.g., authentication, save).
- Refrain from blindly retrying on timeout without exponential backoff.
- Avoid showing generic error messages for timeouts.
- Avoid cancelling requests too aggressively on flaky mobile networks.
- Prevent stale timeouts from overriding new requests, leading to race conditions.
Conclusion: Embrace Failure to Enhance Responsiveness
Timeouts lend your application an aspect of intentionality. They convey the expectation of a successful operation, and if that expectation is not met, the system moves on without wasting further resources.
They safeguard your UX, optimize your compute resources, and assist in debugging real-world issues.
So, don't merely hope for your fetch operations to return.
Determine when to give up on them.
And provide your users with something better than an uncertain wait.
Subscribe for Deep Dives
Get exclusive in-depth technical articles and insights directly to your inbox.
Related Posts
Delving into Server-Sent Events (SSE): Unidirectional Data Streaming Made Simple
A comprehensive exploration of Server-Sent Events (SSE) — a straightforward approach to real-time data transmission from server to browser over HTTP. We'll explore how it operates, its ideal use cases, and how it stands against other technologies like WebSockets and polling.
In-depth Analysis and Understanding of CORS — Cross-Origin Resource Sharing
This article provides a detailed explanation of Cross-Origin Resource Sharing (CORS), its purpose, and how it manages cross-origin requests on the browser. It covers preflight requests, HTTP headers, and potential misconfigurations.
Innovative Resilience: Utilizing Exponential Backoff and Jitter for Retries under Load
This section delves into the intelligent use of retries with exponential backoff and jitter to handle network failures gracefully without overwhelming backend services. It offers practical patterns, real-world examples, and a comprehensive understanding of these techniques.