[JavaScript Series 15] Handling API Errors and Designing Retry UX

한국어 버전

Networks fail all the time. Designing messages, buttons, and logs around API errors keeps users from feeling lost. The essentials for this chapter are “error state message + retry button + backoff order”; online/offline events are optional extras.

Key terms

  1. Error state: the dedicated screen or panel shown when a data request fails.
  2. Retry UX: the flow that defines when and how a user retries after a failure.
  3. Backoff: the strategy of increasing wait times after each failure to avoid hammering the server.
  4. role="alert": an attribute that tells screen readers to announce the region immediately.
  5. online/offline events: browser events that reveal network changes so you can toggle UI states accordingly.

Core ideas

  • Error state: a failure view that includes a message, icon, and retry action whenever data is missing or requests fail.
  • Retry UX: spell out delay intervals, backoff rules, and retry counts so users can recover gracefully.
  • Error categories: distinguish network issues, server errors, and user input errors to deliver targeted messages.
  • Logging and toasts: send details to monitoring while showing concise toasts for end users.

Error flow diagram

UserUI state panelRetry buttonAPI serverLogging service Request datafetchfailure responseshow buttonrecord errorclickretry

In words: “user request → API failure → UI shows a message and logs → user clicks retry → API request again.” Define what the UI must do at every failure point.

Basic error handling

Before tackling retries, keep the simplest state display in mind.

setStatus("Loading");
showRetry(false);
// On failure call setStatus("Please try again shortly")

Every complex flow still relies on two toggles: the status text and whether the retry button is visible.

How to retry failed requests

Now combine retry logic with backoff.

const statusBox = document.querySelector(".status-box");
const retryButton = document.querySelector(".retry-button");

function setStatus(message) {
  if (statusBox) statusBox.textContent = message;
}

function showRetry(show) {
  if (retryButton) retryButton.classList.toggle("is-hidden", !show);
}

async function fetchTasks({ retries = 3, delayMs = 500 } = {}) {
  let attempt = 0;
  while (attempt < retries) {
    try {
      setStatus(`Loading... (attempt ${attempt + 1})`);
      const response = await fetch("/api/tasks");
      if (!response.ok) throw new Error(`Server error ${response.status}`);
      const data = await response.json();
      setStatus("Done");
      return data;
    } catch (error) {
      attempt += 1;
      console.error("API failure", error);
      if (attempt === retries) {
        setStatus("Network is unstable. Please try again in a moment.");
        showRetry(true);
        throw error;
      }
      await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
    }
  }
}

retryButton?.addEventListener("click", () => {
  showRetry(false);
  fetchTasks();
});

Backoff simply means the delay grows with each attempt (delayMs * attempt), lightening the load on the server.

Error cards by category

Render targeted messages for each error type.

function showErrorCard({ title, description, action }) {
  return `
    <div class="error-card" role="alert">
      <h3>${title}</h3>
      <p>${description}</p>
      <button data-action="${action.id}">${action.label}</button>
    </div>
  `;
}

const errorRoot = document.querySelector("#error-root");

function renderError(type) {
  const map = {
    offline: {
      title: "Offline",
      description: "Check your internet connection and try again.",
      action: { id: "reload", label: "Reload" },
    },
    server: {
      title: "Server delay",
      description: "Try again shortly or contact the admin.",
      action: { id: "retry", label: "Retry" },
    },
  };
  if (errorRoot) errorRoot.innerHTML = showErrorCard(map[type]);
}

errorRoot?.addEventListener("click", (event) => {
  const button = event.target;
  if (!(button instanceof HTMLButtonElement)) return;
  if (button.dataset.action === "reload") location.reload();
  if (button.dataset.action === "retry") fetchTasks();
});

Predefining categories like offline and server lets you react quickly to error codes coming back from the backend.

Online/offline awareness

Detect network changes to swap UI states automatically.

window.addEventListener("offline", () => {
  renderError("offline");
});

window.addEventListener("online", () => {
  setStatus("Connected again.");
  fetchTasks();
});

online/offline events are optional—attach them after the core retry UX is solid.

Simulating network conditions and errors

Preview failure scenarios quickly with a BrowserMock component.

http://localhost:5173/tasks
DEGRADED

UI Preview

Design failure states as part of the product

Ship not just the success view but also the wording and actions for every failure.

retry UXbackoffrole=alert

Loading

First request

Status box says loading.

Error

Server delay

role=alert card with retry button.

Recovered

Retry succeeds

List returns and status flips to done.

  • Confirm that the button and message appear together when requests fail.
  • Confirm that error cards disappear once the UI recovers.
  • Confirm that users can immediately tell what to do next.

Why it matters

  • Most user frustration stems from not knowing why something failed; clear error states protect trust.
  • Retry buttons with backoff lower server cost and keep the app resilient.
  • Categorized errors feed directly into monitoring or alerting workflows.

Practice

  • Core follow-along: implement a retry loop like fetchTasks and show all loading/success/failure states.
  • Optional extension: componentize error cards and define messages for offline, server, and validation cases.
  • Debugging: toggle the browser’s offline mode or use a bad URL, then record what appears in both the console and UI.
  • Done when: you can repeat failure → message → retry button → success at least three times without breaking the UI.

Wrap-up

APIs will fail sooner or later. When error states and retry UX are defaults, users can trust the product enough to wait for recovery. Next we will cover routing and page-to-page structure.

💬 댓글

이 글에 대한 의견을 남겨주세요