Go REST

Test 401, 422, 429 and 500 handlers

Force any status code with ?force_status= to make sure your error UI never shows blank screens or eats stack traces.

Every team writes a happy path. Most teams forget about the error paths until something breaks in production. This recipe shows how to systematically exercise every error your client should handle by forcing status codes from the server, so you can verify the user sees something sensible no matter what comes back.

The trick

Append?force_status=N to any Go REST endpoint, and the API returns that status code instead of doing the real work. The response includesX-Simulated-Status: N, so you know the response was forced. The status code respects whatever your client expects: 422 returns a real validation-error body, 429 returns the rate-limit headers, and 500 returns the server-error body.

curl -i "https://gorest.co.in/public/v2/users?force_status=401"
curl -i "https://gorest.co.in/public/v2/users?force_status=422"
curl -i "https://gorest.co.in/public/v2/users?force_status=429"
curl -i "https://gorest.co.in/public/v2/users?force_status=500"

A handler that branches on every case

The pattern: branch on status before the body. 401 is its own thing (sign in again). 422 is its own thing (show field errors). 429 needs a retry. 4xx without a special case is "something looks wrong with the request". 5xx is "the server screwed up". Network errors are their own bucket too.

async function fetchUsers() {
  try {
    const res = await fetch("/public/v2/users?force_status=429", {
      headers: { Authorization: `Bearer ${TOKEN}` }
    });

    if (res.status === 401) {
      return showSignIn();
    }
    if (res.status === 422) {
      const errors = await res.json();
      return showFieldErrors(errors);
    }
    if (res.status === 429) {
      const wait = +res.headers.get("x-ratelimit-reset") || 1;
      return scheduleRetry(wait);
    }
    if (!res.ok) {
      throw new Error(`Got ${res.status}`);
    }

    return await res.json();
  } catch (err) {
    showServerError(err.message);
  }
}

Test it

Three integration tests, one per error type. Use theforce_status parameter so the test never depends on real failure conditions:

// tests/error-handler.test.js
import { test, expect } from "vitest";

test("401 redirects to sign-in", async () => {
  const result = await fetchUsers({ forceStatus: 401 });
  expect(window.location.pathname).toBe("/sign-in");
});

test("422 shows field errors", async () => {
  const result = await fetchUsers({ forceStatus: 422 });
  expect(screen.getByRole("alert")).toHaveTextContent(/can't be blank/i);
});

test("429 schedules retry", async () => {
  const setTimeout = vi.spyOn(globalThis, "setTimeout");
  await fetchUsers({ forceStatus: 429 });
  expect(setTimeout).toHaveBeenCalled();
});

These tests are fast (no real failures, no waiting), deterministic (forced status codes do not vary), and cheap (do not burn rate-limit budget on real 429s).

Map status codes to user-facing messages

Showing"Got 422" in the UI is a signal that you have not thought about errors. Map every code to a sentence the user can understand and act on:

const ERROR_MESSAGES = {
  400: "Something looks wrong with the request. Please refresh.",
  401: "Your session expired. Please sign in again.",
  403: "You do not have permission to do that.",
  404: "We could not find what you were looking for.",
  422: "Some fields need attention.",
  429: "Slow down a moment. We are throttling requests.",
  500: "Something broke on our side. Please try again.",
  503: "We are briefly unavailable. Try in a moment."
};

function userFacingError(status) {
  return ERROR_MESSAGES[status] || "Something went wrong.";
}

The 401 message implies the user should sign in. The 422 message implies they should look at the form. The 429 message implies they should wait. Each is actionable.

Build a "kitchen sink" page for QA

A small page in your app that lets QA force every error in turn is worth its weight. Buttons that hit the API with eachforce_status value, plus a third for "delay 5s", plus a fourth for "no network" (wrap fetch in a Promise that rejects). One review, every error path covered.

Common mistakes

Combine with simulated latency

Thedelay andforce_status parameters compose. Test "slow then 500":

fetch("/public/v2/users?delay=3000&force_status=500")

This is the worst case for many UIs: a long wait, then an error. Use it to verify your skeleton + error transition does not flash, race, or leave the page in an inconsistent state.

More recipes

Keep building

All recipes Integration guides