Lesson 2 of 2 · Resilient Error Handling (try/catch)
Loading, Retry & Timeouts
Make requests understandable by showing progress, results and a useful recovery path.
This lesson is locked. You can read ahead, but finish earlier stops to earn progress.
Listen
Watch
Read
Model three request states
A request-driven interface should communicate three states: loading, success and error. Loading means work is in progress; success shows the returned content; error explains that the attempt failed. If the interface shows nothing while waiting, users may think the app is frozen. Your quest asks you to add a loading skeleton and handle awkward queries gracefully, so state changes should be visible.
Set loading before starting the request, then replace it with results on success or an error message on failure. Keep these states mutually clear so old content does not accidentally look like the new response.
Skeletons, spinners and retry
A skeleton is a placeholder shaped like the content that will arrive, while a spinner signals activity more generally. Both tell users that the app is working. Use a skeleton when the rough layout is known; use a spinner for a small, general wait.
If a request fails, offer a Retry button when trying again is reasonable. The button should repeat the same operation and return the interface to loading first. Disable it briefly while a request is in progress to avoid accidental duplicate attempts. For search queries, handle empty or unusual input with a helpful message rather than a broken-looking result.
Put a limit on waiting
Some requests can take too long. An AbortController lets code cancel a fetch by passing its signal in the request options and calling abort() later. A timeout can trigger that cancellation, after which your normal error handling displays a recovery message.
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch('/api/items', { signal: controller.signal });
} finally { clearTimeout(timer); }This fragment shows the cancellation pattern; combine it with response checks and error UI in your app. Clear the timer when the request finishes so it does not fire unnecessarily.
Test the whole experience
Verify that loading appears promptly, success replaces it with data, and errors replace or explain the failed content. Test a retry and a slow or cancelled request if your environment allows it. These transitions make an app feel responsive even when the network is unpredictable.
A polished error path is part of the feature, not an optional extra. Users should know what is happening and what they can do next.
Walk one recovery flow before adding timeouts
Use a stable container and keep the Retry button outside the area you replace with results. This prevents throwing away the button or attaching another listener on every render. For a small rehearsal:
<p id="request-state" aria-live="polite">Ready</p> <button id="retry-request">Load / retry</button>
Study the request function below. Adapt the idea to your dashboard rather than replacing your whole assessment with this single-label rehearsal:
const status = document.getElementById('request-state');
const retry = document.getElementById('retry-request');
let pending = false;
async function load() {
if (!status || !retry || pending) return;
pending = true;
retry.disabled = true;
status.textContent = 'Loading…';
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://official-joke-api.appspot.com/random_joke', {
signal: controller.signal
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (typeof data.setup !== 'string') throw new Error('Unexpected data');
status.textContent = data.setup;
} catch {
status.textContent = controller.signal.aborted
? 'Request took too long. Retry when ready.'
: 'Could not load. Retry when ready.';
} finally {
clearTimeout(timer);
pending = false;
retry.disabled = false;
}
}
retry?.addEventListener('click', load);The pending guard stops overlapping calls; disabling the control communicates that guard. finally runs after either success or catch, so cleanup does not depend on which outcome happened. The listener is registered once. Abort stops this client waiting; it is not proof a server-side write was undone.
Rehearse visible states, not only logs
In DevTools Network, choose a slow connection and click once: loading should appear immediately. Temporarily use a missing route and expect an error; restore the route and retry. Choose Offline and try again; return to online and confirm recovery. Restore normal throttling afterward.
Test repeated clicks: one pending operation, not a growing number of Network rows per click. If the spinner never ends, inspect the catch and finally paths. For a skeleton, reserve the layout's shape but keep the same meaningful status text; do not leave a moving placeholder after failure.
Retry is appropriate for a harmless GET after a transient failure. Never automatically repeat a payment or other non-idempotent POST: the server may have processed it even if the client timed out. Search interfaces that allow overlapping requests need cancellation or request IDs to prevent an older response overwriting newer results; this rehearsal prevents overlap instead.
Before activity 3.3, demonstrate success, loading, bad-query error and a working retry in the visible page. A timeout and duplicate-request test deepen that understanding; they do not introduce a new unlock requirement.
Key terms
- loading state
- Interface feedback shown while a request is in progress.
- skeleton
- A placeholder that resembles the shape of pending content.
- retry
- A new attempt after an operation has failed.
- AbortController
- A browser API that can cancel fetch requests.
Summary — copy into your notes
- Represent requests with loading, success and error states.
- Use skeletons or spinners to show that work is happening.
- Offer a Retry button when another attempt makes sense.
- AbortController can cancel a request that takes too long.