errors
Generation Error
A console-style error card for a failed generation, with details and retry.
Something went wrong
The response couldn't be generated.
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { ChevronDown, CircleAlert, RotateCcw } from "lucide-react";
import { cn } from "@/lib/utils";
export type GenerationErrorReason = "network" | "server" | "timeout" | "filtered";
export interface GenerationErrorProps {
reason?: GenerationErrorReason;
message?: string;
errorCode?: string;
onRetry?: () => void;
retrying?: boolean;
className?: string;
}
const reasonConfig: Record<GenerationErrorReason, { title: string; hint: string; retryable: boolean }> = {
network: {
title: "Connection lost",
hint: "Check your connection and try again.",
retryable: true,
},
server: {
title: "Something went wrong",
hint: "The response couldn't be generated.",
retryable: true,
},
timeout: {
title: "Request timed out",
hint: "The model took too long to respond.",
retryable: true,
},
filtered: {
title: "Response blocked",
hint: "This request was blocked by content safety filters.",
retryable: false,
},
};
export function GenerationError({
reason = "server",
message,
errorCode,
onRetry,
retrying = false,
className,
}: GenerationErrorProps) {
const [detailsOpen, setDetailsOpen] = React.useState(false);
const config = reasonConfig[reason];
return (
<div
role="alert"
className={cn("flex flex-col gap-0 rounded-2xl border border-border bg-muted/30 overflow-hidden", className)}
>
<div className="flex items-start gap-3 px-4 py-3">
<CircleAlert className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden />
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<p className="text-xs font-medium text-foreground">{config.title}</p>
<p className="text-[11px] text-muted-foreground">{message ?? config.hint}</p>
</div>
</div>
<div className="flex items-center justify-between border-t border-border/60 bg-muted/40 px-4 py-2.5">
{errorCode ? (
<button
type="button"
onClick={() => setDetailsOpen((v) => !v)}
aria-expanded={detailsOpen}
className="flex items-center gap-1 text-[11px] text-muted-foreground transition-colors hover:text-foreground"
>
<ChevronDown
className={cn("size-3 transition-transform", detailsOpen && "rotate-180")}
aria-hidden
/>
Show details
</button>
) : (
<span />
)}
{config.retryable && onRetry && (
<button
type="button"
onClick={onRetry}
disabled={retrying}
aria-label={retrying ? "Retrying" : "Retry"}
className="flex items-center gap-1 rounded-full bg-foreground px-2.5 py-1 text-[11px] font-medium text-background transition-opacity hover:opacity-80 disabled:opacity-50"
>
<RotateCcw className={cn("size-3", retrying && "animate-spin")} aria-hidden />
{retrying ? "Retrying…" : "Retry"}
</button>
)}
</div>
<AnimatePresence initial={false}>
{detailsOpen && errorCode && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.18, ease: "easeOut" }}
className="overflow-hidden border-t border-border/60"
>
<pre className="whitespace-pre-wrap break-all px-4 py-2.5 font-mono text-[11px] text-muted-foreground">
{errorCode}
</pre>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
A UX spec for this pattern — written for agents implementing or reusing it, not the code.
# Generation Error
## Summary
A message-thread card for a generation that produced no output at all — a network drop, a server failure, a timeout, or a content-safety block. Shows a short human-readable reason, an optional collapsible technical detail (a console-style error line), and a Retry action where retrying makes sense.
## When to use
- The request never produced any content — nothing to preserve, unlike a cut-short stream.
- In the message thread at the position of the failed assistant turn, replacing the loader that was there.
- Whenever a technical error code, request ID, or stack-like detail exists and might help a user reporting the issue — surface it behind "Show details" rather than inline.
## When not to use
- Some content was generated before the failure — use the Partial Response pattern instead so the user doesn't lose it.
- Quota/usage limits — use the Rate Limit pattern; those are expected, recurring states, not failures.
- Validation errors on the user's own input (empty prompt, unsupported file type) — handle inline in the composer, not as a thread message.
## Reasons
Four distinct reasons change the copy and whether Retry is offered:
- **network**: Connection dropped mid-request. Title "Connection lost". Retryable.
- **server**: Upstream failure. Title "Something went wrong". Retryable.
- **timeout**: The model didn't respond in time. Title "Request timed out". Retryable.
- **filtered**: Blocked by content-safety policy. Title "Response blocked". Not retryable — retrying the same prompt will fail the same way; the user needs to change the request instead.
## Anatomy
- Icon: a muted alert glyph, not an alarming red — consistent with this library's other error/limit states.
- Title (bold) + one-line hint, muted.
- Footer row: "Show details" toggle on the left (only rendered when `errorCode` is passed), Retry button on the right (only when the reason is retryable and `onRetry` is passed).
- Details panel: a monospace, console-style line with the technical error code / request ID, expandable under the footer.
## Behavior
- Details panel animates open/closed by height, collapsed by default.
- Retry enters a disabled "Retrying…" state immediately on tap; the spinner icon rotates. The caller owns the actual re-generation call and clears `retrying` when it resolves (success replaces this component; failure re-renders it).
- Non-retryable reasons (`filtered`) never show a Retry button even if `onRetry` is passed — the caller should instead let the user edit and resend their prompt.
- `message` overrides the default hint text per reason, for surfacing a more specific server-provided message without changing the title or retryability.
## Content guidelines
- Titles are short and human: "Connection lost", "Something went wrong", "Request timed out", "Response blocked" — never a raw HTTP status or exception class name in the title.
- The hint is one sentence, plain language, no blame ("Check your connection and try again," not "Your connection failed").
- Technical detail (`errorCode`) is the only place raw identifiers belong — request IDs, error codes, stack fragments — and it's opt-in behind "Show details", never shown by default.
- Retry label is always "Retry", not "Try again" or "Regenerate" — keep it distinct from the Partial Response pattern's "Continue"/"Retry" pair.
## Accessibility
- The card has `role="alert"` so screen readers announce the failure as it mounts.
- "Show details" uses `aria-expanded` reflecting panel state.
- The Retry button's `aria-label` switches to "Retrying" while in flight, since the visible label ("Retrying…") pairs with a spinning icon that conveys nothing to assistive tech on its own.
- The alert icon is `aria-hidden` — the card's text content, not the icon, carries the meaning.
## Related patterns
- Partial Response — for streams that produced some content before stopping.
- Rate Limit — for quota-exceeded states, which are expected and recurring rather than failures.