Stockio

Loading Skeletons and Error States for Async API Data

Build resilient API interfaces with explicit loading states, stable skeletons, cancellation, and useful recovery actions that keep users in control.

By Sofia Marchetti

JavaScript Engineer ·

On this page

An API request is not a binary choice between a spinner and a result. It can succeed with no records, fail after showing cached content, or finish after the user has already changed filters. Interfaces that ignore those distinctions feel unreliable even when the server works correctly.

This guide builds a practical state model, connects it to cancellation, and explains how to design loading and recovery experiences without moving the page around. The goal is predictable behavior under slow connections, not merely a polished happy path.

Measure the wait before designing it

Start with observed latency, not an animation preference. Use the API Response Latency & Status Tester below to inspect a public endpoint from your current browser. Compare several requests rather than treating one fast response as representative. Browser location, connection reuse, caching, and server load all change the result.

The API latency tester is a useful diagnostic, not a production percentile dashboard. Cross-origin restrictions may prevent browser access even when an endpoint is healthy. Never paste private credentials into a public testing workflow, and use your monitoring system for authenticated production measurements.

Define a feedback budget for each interaction. A search field needs prompt acknowledgment; a report export may take much longer and deserve a job status screen. Consider delaying a skeleton briefly to avoid flashing it during fast responses, but acknowledge button presses immediately. A short delay is a design choice to test, not a universal performance threshold.

Model states instead of stacking flags

Three independent booleans such as loading, error, and success allow impossible combinations. A discriminated union makes valid states explicit and forces rendering code to handle each branch. Keep initial loading separate from background refresh, because existing content remains useful while newer data arrives.

The following browser TypeScript example demonstrates initial loading and cancellation. Runtime validation checks the response shape before it becomes application data; a TypeScript assertion alone would not protect that boundary.

type Item = { id: string; title: string };
type State =
| { status: "loading" }
| { status: "success"; items: Item[] }
| { status: "error"; message: string };

function isItems(value: unknown): value is Item[] {
return Array.isArray(value) && value.every(item =>
  typeof item === "object" && item !== null &&
  typeof item.id === "string" &&
  typeof item.title === "string"
);
}

function loadItems(render: (state: State) => void) {
const controller = new AbortController();
render({ status: "loading" });

void (async () => {
  try {
    const response = await fetch("/api/items", {
      signal: controller.signal
    });
    if (!response.ok) throw new Error("Request failed");
    const data: unknown = await response.json();
    if (!isItems(data)) throw new Error("Invalid response");
    if (controller.signal.aborted) return;
    render({ status: "success", items: data });
  } catch {
    if (controller.signal.aborted) return;
    render({ status: "error", message: "Could not load items." });
  }
})();

return () => controller.abort();
}

Call the returned cleanup function before starting a replacement request and when the view unmounts. For overlapping requests that cannot be canceled, also compare a monotonically increasing request identifier before committing results. Otherwise, an older search can overwrite a newer answer.

Read the Fetch API documentation for the distinction between network rejection and HTTP error responses. Fetch does not reject simply because the server returns a 404 or 500. Checking response.ok is therefore part of normal control flow, not an optional enhancement.

Make skeletons preserve structure

A good skeleton communicates content geometry. Match the approximate height of the eventual card, thumbnail ratio, and spacing between repeated rows. Avoid a full-page gray imitation of every interface detail: it creates visual noise without giving users more useful information.

Reserve image dimensions before the network request finishes. Give text placeholders a representative number of lines and let the real content grow naturally when required. Do not freeze a variable-length article excerpt inside a fixed height merely to make a screenshot look stable. The companion guide on fixing layout shifts covers the container side of this problem.

Use quiet motion or no motion. A shimmer running across twenty cards can distract from navigation and increase rendering work on low-end devices. Honor reduced-motion preferences and keep placeholders out of the accessibility tree when they contain no meaningful content.

Expose one concise loading message through an appropriate status region, and consider marking the results container busy while it updates. The WAI status message guidance explains how updates can be available to assistive technology without unnecessarily moving focus. Test actual announcements; adding live regions everywhere often produces repeated speech.

Turn failures into recoverable decisions

An error panel needs context and an action, not an implementation exception. “Could not load invoices. Try again” is better than “Unexpected token.” Keep diagnostic details in protected logs, with a request identifier that support staff can use without exposing tokens or personal information.

Different failures deserve different recovery paths. A missing record may need a route back to the list. An expired session may need authentication. A rate limit should respect server retry guidance. A validation error belongs beside the relevant field rather than inside a generic page banner.

  • Keep entered form values after a failed submission.
  • Preserve existing results when a refresh fails, and label them as potentially outdated.
  • Offer a visible retry button for recoverable reads.
  • Explain when an operation may already have completed before a timeout.

Automatic retries are appropriate only when their consequences are understood. Retrying a read is usually simpler than retrying a payment. For mutations, use server-supported idempotency and a stable operation identifier where appropriate. Add bounded exponential backoff with jitter for transient failures, and stop when the view is abandoned.

Consult the AbortController reference when implementing cancellation. Canceling a browser request does not guarantee that the server stopped processing it, so cancellation must not be presented as undoing a completed business action.

Test the transitions, not just the screens

Create a transition matrix before review. Include initial load, empty success, nonempty success, malformed JSON, HTTP failure, offline failure, cancellation, and a slow response followed by a faster replacement. Add background-refresh failure if the application retains cached content.

Use browser throttling and deterministic mocked responses to reproduce these scenarios. A manually clicked retry button should not produce duplicate submissions or competing status announcements. Navigate away during loading, then return: the interface should not display stale errors from the previous visit.

Measure user-visible milestones separately from raw endpoint duration. Response headers arriving does not mean JSON parsing, rendering, and image loading have completed. Revisit the response timing tool to isolate endpoint behavior, then compare it with browser performance traces and application telemetry.

Make acceptance criteria concrete: the results region keeps its place, focus remains usable, the latest query wins, and every visible failure offers a sensible next step. These checks are inexpensive compared with debugging intermittent reports after release.

Key takeaways

  • Define explicit states for initial loading, empty results, success, errors, and background refresh.
  • Prevent stale responses with cancellation and request identity checks where needed.
  • Build skeletons around real content dimensions and accessible status announcements.
  • Preserve useful content and user input when requests fail.
  • Test transitions under controlled delays rather than approving isolated screenshots.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in JavaScript & Frameworks
JavaScript & Frameworks

SSR vs SSG vs ISR vs SPA: Choosing a Rendering Strategy

Choose rendering strategies by freshness, personalization, and failure tolerance. Compare SSR, SSG, ISR, and SPA behavior with route-level examples and practical tests.

Includes: API latencySofia MarchettiSofia Marchetti
JavaScript & Frameworks

Debounce vs Throttle in JavaScript (With Code)

Choose debounce or throttle for search, resize, and pointer events. Implement cancellable helpers, understand timing tradeoffs, and prevent stale asynchronous results.

Includes: API latencyDaniel KimDaniel Kim
JavaScript & Frameworks

AbortController: Cancel Fetch Requests and Set Timeouts

Cancel browser fetch requests, implement reliable timeouts, and prevent stale responses with AbortController while keeping cleanup and user-facing errors predictable.

Includes: API latencyDaniel KimDaniel Kim
JavaScript & Frameworks

Promise.all vs allSettled vs race vs any in JavaScript

Choose Promise.all, allSettled, race, or any based on failure behavior, then control concurrency and test asynchronous workflows without accidental serial execution.

Includes: API latencyDaniel KimDaniel Kim
DevOps & Deployment

10 Best Uptime Monitoring Tools

Compare ten uptime monitoring services for websites and APIs, from simple availability checks to incident workflows. Learn what each is suited to, what it cannot tell you, and how to choose.

Includes: API latencyRavi DesaiRavi Desai
Backend & APIs

12 Best Free Public APIs for Side Projects

Find approachable public APIs for weather, books, countries, and sample data, plus practical notes on access, limits, and choosing an endpoint for a side project.

Includes: API latencyChidi NwosuChidi Nwosu