{"slug":"generation-error","category":"errors","job":"recover","added":"2026-09-14T09:58Z","title":"Generation Error","description":"A console-style error card for a failed generation, with details and retry.","url":"https://ai-patterns.app/patterns/errors/generation-error","markdown":"https://ai-patterns.app/patterns/errors/generation-error.md","json":"https://ai-patterns.app/api/patterns/errors/generation-error","seenIn":[{"team":"Claude","source":"https://support.claude.com/en/articles/12466728-understand-claude-error-messages"}],"spec":"# Generation Error\n\n## Summary\nA 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.\n\n## When to use\n- The request never produced any content — nothing to preserve, unlike a cut-short stream.\n- In the message thread at the position of the failed assistant turn, replacing the loader that was there.\n- 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.\n\n## When not to use\n- Some content was generated before the failure — use the Partial Response pattern instead so the user doesn't lose it.\n- Quota/usage limits — use the Rate Limit pattern; those are expected, recurring states, not failures.\n- Validation errors on the user's own input (empty prompt, unsupported file type) — handle inline in the composer, not as a thread message.\n\n## Reasons\nFour distinct reasons change the copy and whether Retry is offered:\n\n- **network**: Connection dropped mid-request. Title \"Connection lost\". Retryable.\n- **server**: Upstream failure. Title \"Something went wrong\". Retryable.\n- **timeout**: The model didn't respond in time. Title \"Request timed out\". Retryable.\n- **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.\n\n## Anatomy\n- Icon: a muted alert glyph, not an alarming red — consistent with this library's other error/limit states.\n- Title (bold) + one-line hint, muted.\n- 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).\n- Details panel: a monospace, console-style line with the technical error code / request ID, expandable under the footer.\n\n## Behavior\n- Details panel animates open/closed by height, collapsed by default.\n- 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).\n- 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.\n- `message` overrides the default hint text per reason, for surfacing a more specific server-provided message without changing the title or retryability.\n\n## Content guidelines\n- 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.\n- The hint is one sentence, plain language, no blame (\"Check your connection and try again,\" not \"Your connection failed\").\n- 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.\n- Retry label is always \"Retry\", not \"Try again\" or \"Regenerate\" — keep it distinct from the Partial Response pattern's \"Continue\"/\"Retry\" pair.\n\n## Accessibility\n- The card has `role=\"alert\"` so screen readers announce the failure as it mounts.\n- \"Show details\" uses `aria-expanded` reflecting panel state.\n- 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.\n- The alert icon is `aria-hidden` — the card's text content, not the icon, carries the meaning.\n\n## Related patterns\n- **Partial Response** — Use it instead when the stream produced some content before it stopped.\n- **Rate Limit** — Use it instead for quota-exceeded states, which are expected and recurring rather than failures.\n- **Connectivity Error** — Use it instead when the request failed because the person went offline.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { ChevronDown, CircleAlert, RotateCcw } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type GenerationErrorReason = \"network\" | \"server\" | \"timeout\" | \"filtered\";\n\nexport interface GenerationErrorProps {\n  reason?: GenerationErrorReason;\n  message?: string;\n  errorCode?: string;\n  onRetry?: () => void;\n  retrying?: boolean;\n  className?: string;\n}\n\nconst reasonConfig: Record<GenerationErrorReason, { title: string; hint: string; retryable: boolean }> = {\n  network: {\n    title: \"Connection lost\",\n    hint: \"Check your connection and try again.\",\n    retryable: true,\n  },\n  server: {\n    title: \"Something went wrong\",\n    hint: \"The response couldn't be generated.\",\n    retryable: true,\n  },\n  timeout: {\n    title: \"Request timed out\",\n    hint: \"The model took too long to respond.\",\n    retryable: true,\n  },\n  filtered: {\n    title: \"Response blocked\",\n    hint: \"This request was blocked by content safety filters.\",\n    retryable: false,\n  },\n};\n\nexport function GenerationError({\n  reason = \"server\",\n  message,\n  errorCode,\n  onRetry,\n  retrying = false,\n  className,\n}: GenerationErrorProps) {\n  const [detailsOpen, setDetailsOpen] = React.useState(false);\n  const config = reasonConfig[reason];\n\n  return (\n    <div\n      role=\"alert\"\n      className={cn(\"flex flex-col gap-0 rounded-2xl border border-border bg-muted/30 overflow-hidden\", className)}\n    >\n      <div className=\"flex items-start gap-3 px-4 py-3\">\n        <CircleAlert className=\"mt-0.5 size-4 shrink-0 text-muted-foreground\" aria-hidden />\n\n        <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n          <p className=\"text-xs font-medium text-foreground\">{config.title}</p>\n          <p className=\"text-[11px] text-muted-foreground\">{message ?? config.hint}</p>\n        </div>\n      </div>\n\n      <div className=\"flex items-center justify-between border-t border-border/60 bg-muted/40 px-4 py-2.5\">\n        {errorCode ? (\n          <button\n            type=\"button\"\n            onClick={() => setDetailsOpen((v) => !v)}\n            aria-expanded={detailsOpen}\n            className=\"flex items-center gap-1 text-[11px] text-muted-foreground transition-colors hover:text-foreground\"\n          >\n            <ChevronDown\n              className={cn(\"size-3 transition-transform\", detailsOpen && \"rotate-180\")}\n              aria-hidden\n            />\n            Show details\n          </button>\n        ) : (\n          <span />\n        )}\n\n        {config.retryable && onRetry && (\n          <button\n            type=\"button\"\n            onClick={onRetry}\n            disabled={retrying}\n            aria-label={retrying ? \"Retrying\" : \"Retry\"}\n            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\"\n          >\n            <RotateCcw className={cn(\"size-3\", retrying && \"animate-spin\")} aria-hidden />\n            {retrying ? \"Retrying…\" : \"Retry\"}\n          </button>\n        )}\n      </div>\n\n      <AnimatePresence initial={false}>\n        {detailsOpen && errorCode && (\n          <motion.div\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={{ duration: 0.18, ease: \"easeOut\" }}\n            className=\"overflow-hidden border-t border-border/60\"\n          >\n            <pre className=\"whitespace-pre-wrap break-all px-4 py-2.5 font-mono text-[11px] text-muted-foreground\">\n              {errorCode}\n            </pre>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}