Error Handling & UX

Design for failures and slow responses so users always get a graceful experience.

AI features fail differently from ordinary code: responses can be slow, rate-limited, or occasionally refused. Good UX plans for all three.

Practical patterns

  • Show progress — stream the response, or show a typing indicator, so users know something is happening.
  • Fail gracefully — if a call errors, show a friendly message and a retry, not a stack trace.
  • Handle refusals — Claude can decline unsafe requests (stop_reason: "refusal"); handle that as a normal outcome.
  • Set timeouts — do not let a slow call hang the UI forever.

Example

Example · javascript
async function safeAsk(question) {
  try {
    const res = await client.messages.create({
      model: "claude-opus-4-8", max_tokens: 1024,
      messages: [{ role: "user", content: question }],
    });
    if (res.stop_reason === "refusal") {
      return "Sorry, I can't help with that request.";
    }
    return res.content.find((b) => b.type === "text")?.text ?? "";
  } catch (err) {
    return "Something went wrong. Please try again in a moment.";
  }
}

When to use it

  • Show a skeleton loader while waiting for Claude and replace it with an error message if the request times out.
  • Queue a failed AI request for a background retry rather than losing the user's task when Claude is briefly overloaded.
  • Log every Claude error with the request ID so you can reproduce and debug production failures from structured logs.

More examples

Show loading and error states in React

Uses a state machine (loading/error/result) so the UI always shows the right state to the user.

Example · javascript
const [state, setState] = useState({ loading: false, error: null, result: null });

async function askClaude(question) {
  setState({ loading: true, error: null, result: null });
  try {
    const res = await fetch("/api/ask", {
      method: "POST",
      body: JSON.stringify({ question }),
      headers: { "Content-Type": "application/json" },
    });
    if (!res.ok) throw new Error(`Server error ${res.status}`);
    const { answer } = await res.json();
    setState({ loading: false, error: null, result: answer });
  } catch (err) {
    setState({ loading: false, error: "Something went wrong. Please try again.", result: null });
  }
}

Timeout a slow Claude request

Attaches an AbortController to the fetch so long-running Claude requests fail gracefully after 30 seconds.

Example · javascript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30_000); // 30 s

try {
  const res = await fetch("/api/ask", {
    method: "POST",
    body: JSON.stringify({ question }),
    signal: controller.signal,
  });
  const data = await res.json();
  return data.answer;
} catch (err) {
  if (err.name === "AbortError") return "Request timed out. Please try again.";
  throw err;
} finally {
  clearTimeout(timeout);
}

Server-side error logging with context

Logs a structured error object server-side and returns a clean 503 to the browser instead of leaking the raw error.

Example · javascript
export async function POST(req) {
  const { question } = await req.json();
  try {
    const msg = await client.messages.create({
      model: "claude-haiku-4-5",
      max_tokens: 256,
      messages: [{ role: "user", content: question }],
    });
    return Response.json({ answer: msg.content[0].text });
  } catch (err) {
    console.error({ event: "claude_error", status: err.status, message: err.message, question });
    return Response.json({ error: "AI unavailable" }, { status: 503 });
  }
}

Discussion

  • Be the first to comment on this lesson.