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
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.
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.
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.
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