[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M79FWRPVYK0MFC9AQAP63SS3":3,"skill-same:01M79FWRPVYK0MFC9AQAP63SS3":47},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":18,"linkOnly":19,"markdownContent":20,"markdownBody":21,"frontMatter":22,"refName":25,"commitSha":26,"skillDirectory":27,"forksCount":28,"repositoryHtmlUrl":29,"repositoryHomepageUrl":30,"ownerHtmlUrl":31,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":32,"install":33,"resources":46},"01M79FWRPVYK0MFC9AQAP63SS3","error-handling",null,"Patterns for robust error handling across TypeScript, Python, and Go. Covers typed errors, error boundaries, retries, circuit breakers, and user-facing error messages.","teamai-hub\u002Fteamai-template","teamai-hub","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F306071925?v=4","01M79FWC6MTYWMNQ6B30CW3JTX","01M79FWC76A0S3SP1JJWD369YV",22,"MIT","https:\u002F\u002Fgithub.com\u002Fteamai-hub\u002Fteamai-template\u002Fblob\u002Fmain\u002Fskills\u002Fecc\u002Ferror-handling\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: error-handling\ndescription: Patterns for robust error handling across TypeScript, Python, and Go. Covers typed errors, error boundaries, retries, circuit breakers, and user-facing error messages.\nmetadata:\n  origin: ECC\n---\n\n# Error Handling Patterns\n\nConsistent, robust error handling patterns for production applications.\n\n## When to Activate\n\n- Designing error types or exception hierarchies for a new module or service\n- Adding retry logic or circuit breakers for unreliable external dependencies\n- Reviewing API endpoints for missing error handling\n- Implementing user-facing error messages and feedback\n- Debugging cascading failures or silent error swallowing\n\n## Core Principles\n\n1. **Fail fast and loudly** — surface errors at the boundary where they occur; don't bury them\n2. **Typed errors over string messages** — errors are first-class values with structure\n3. **User messages ≠ developer messages** — show friendly text to users, log full context server-side\n4. **Never swallow errors silently** — every `catch` block must either handle, re-throw, or log\n5. **Errors are part of your API contract** — document every error code a client may receive\n\n## TypeScript \u002F JavaScript\n\n### Typed Error Classes\n\n```typescript\n\u002F\u002F Define an error hierarchy for your domain\nexport class AppError extends Error {\n  constructor(\n    message: string,\n    public readonly code: string,\n    public readonly statusCode: number = 500,\n    public readonly details?: unknown,\n  ) {\n    super(message)\n    this.name = this.constructor.name\n    \u002F\u002F Maintain correct prototype chain in transpiled ES5 JavaScript.\n    \u002F\u002F Required for `instanceof` checks (e.g., `error instanceof NotFoundError`)\n    \u002F\u002F to work correctly when extending the built-in Error class.\n    Object.setPrototypeOf(this, new.target.prototype)\n  }\n}\n\nexport class NotFoundError extends AppError {\n  constructor(resource: string, id: string) {\n    super(`${resource} not found: ${id}`, 'NOT_FOUND', 404)\n  }\n}\n\nexport class ValidationError extends AppError {\n  constructor(message: string, details: { field: string; message: string }[]) {\n    super(message, 'VALIDATION_ERROR', 422, details)\n  }\n}\n\nexport class UnauthorizedError extends AppError {\n  constructor(reason = 'Authentication required') {\n    super(reason, 'UNAUTHORIZED', 401)\n  }\n}\n\nexport class RateLimitError extends AppError {\n  constructor(public readonly retryAfterMs: number) {\n    super('Rate limit exceeded', 'RATE_LIMITED', 429)\n  }\n}\n```\n\n### Result Pattern (no-throw style)\n\nFor operations where failure is expected and common (parsing, external calls):\n\n```typescript\ntype Result\u003CT, E = AppError> =\n  | { ok: true; value: T }\n  | { ok: false; error: E }\n\nfunction ok\u003CT>(value: T): Result\u003CT> {\n  return { ok: true, value }\n}\n\nfunction err\u003CE>(error: E): Result\u003Cnever, E> {\n  return { ok: false, error }\n}\n\n\u002F\u002F Usage\nasync function fetchUser(id: string): Promise\u003CResult\u003CUser>> {\n  try {\n    const user = await db.users.findUnique({ where: { id } })\n    if (!user) return err(new NotFoundError('User', id))\n    return ok(user)\n  } catch (e) {\n    return err(new AppError('Database error', 'DB_ERROR'))\n  }\n}\n\nconst result = await fetchUser('abc-123')\nif (!result.ok) {\n  \u002F\u002F TypeScript knows result.error here\n  logger.error('Failed to fetch user', { error: result.error })\n  return\n}\n\u002F\u002F TypeScript knows result.value here\nconsole.log(result.value.email)\n```\n\n### API Error Handler (Next.js \u002F Express)\n\n```typescript\nimport { NextRequest, NextResponse } from 'next\u002Fserver'\n\nfunction handleApiError(error: unknown): NextResponse {\n  \u002F\u002F Known application error\n  if (error instanceof AppError) {\n    return NextResponse.json(\n      {\n        error: {\n          code: error.code,\n          message: error.message,\n          ...(error.details ? { details: error.details } : {}),\n        },\n      },\n      { status: error.statusCode },\n    )\n  }\n\n  \u002F\u002F Zod validation error\n  if (error instanceof z.ZodError) {\n    return NextResponse.json(\n      {\n        error: {\n          code: 'VALIDATION_ERROR',\n          message: 'Request validation failed',\n          details: error.issues.map(i => ({\n            field: i.path.join('.'),\n            message: i.message,\n          })),\n        },\n      },\n      { status: 422 },\n    )\n  }\n\n  \u002F\u002F Unexpected error — log details, return generic message\n  console.error('Unexpected error:', error)\n  return NextResponse.json(\n    { error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } },\n    { status: 500 },\n  )\n}\n\nexport async function POST(req: NextRequest) {\n  try {\n    \u002F\u002F ... handler logic\n  } catch (error) {\n    return handleApiError(error)\n  }\n}\n```\n\n### React Error Boundary\n\n```typescript\nimport { Component, ErrorInfo, ReactNode } from 'react'\n\ninterface Props {\n  fallback: ReactNode\n  onError?: (error: Error, info: ErrorInfo) => void\n  children: ReactNode\n}\n\ninterface State {\n  hasError: boolean\n  error: Error | null\n}\n\nexport class ErrorBoundary extends Component\u003CProps, State> {\n  state: State = { hasError: false, error: null }\n\n  static getDerivedStateFromError(error: Error): State {\n    return { hasError: true, error }\n  }\n\n  componentDidCatch(error: Error, info: ErrorInfo) {\n    this.props.onError?.(error, info)\n    console.error('Unhandled React error:', error, info)\n  }\n\n  render() {\n    if (this.state.hasError) return this.props.fallback\n    return this.props.children\n  }\n}\n\n\u002F\u002F Usage\n\u003CErrorBoundary fallback={\u003Cp>Something went wrong. Please refresh.\u003C\u002Fp>}>\n  \u003CMyComponent \u002F>\n\u003C\u002FErrorBoundary>\n```\n\n## Python\n\n### Custom Exception Hierarchy\n\n```python\nclass AppError(Exception):\n    \"\"\"Base application error.\"\"\"\n    def __init__(self, message: str, code: str, status_code: int = 500):\n        super().__init__(message)\n        self.code = code\n        self.status_code = status_code\n\nclass NotFoundError(AppError):\n    def __init__(self, resource: str, id: str):\n        super().__init__(f\"{resource} not found: {id}\", \"NOT_FOUND\", 404)\n\nclass ValidationError(AppError):\n    def __init__(self, message: str, details: list[dict] | None = None):\n        super().__init__(message, \"VALIDATION_ERROR\", 422)\n        self.details = details or []\n```\n\n### FastAPI Global Exception Handler\n\n```python\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import JSONResponse\n\napp = FastAPI()\n\n@app.exception_handler(AppError)\nasync def app_error_handler(request: Request, exc: AppError) -> JSONResponse:\n    return JSONResponse(\n        status_code=exc.status_code,\n        content={\"error\": {\"code\": exc.code, \"message\": str(exc)}},\n    )\n\n@app.exception_handler(Exception)\nasync def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:\n    # Log full details, return generic message\n    logger.exception(\"Unexpected error\", exc_info=exc)\n    return JSONResponse(\n        status_code=500,\n        content={\"error\": {\"code\": \"INTERNAL_ERROR\", \"message\": \"An unexpected error occurred\"}},\n    )\n```\n\n## Go\n\n### Sentinel Errors and Error Wrapping\n\n```go\npackage domain\n\nimport \"errors\"\n\n\u002F\u002F Sentinel errors for type-checking\nvar (\n    ErrNotFound    = errors.New(\"not found\")\n    ErrUnauthorized = errors.New(\"unauthorized\")\n    ErrConflict     = errors.New(\"conflict\")\n)\n\n\u002F\u002F Wrap errors with context — never lose the original\nfunc (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {\n    user, err := r.db.QueryRow(ctx, \"SELECT * FROM users WHERE id = $1\", id)\n    if errors.Is(err, sql.ErrNoRows) {\n        return nil, fmt.Errorf(\"user %s: %w\", id, ErrNotFound)\n    }\n    if err != nil {\n        return nil, fmt.Errorf(\"querying user %s: %w\", id, err)\n    }\n    return user, nil\n}\n\n\u002F\u002F At the handler level, unwrap to determine response\nfunc (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {\n    user, err := h.service.GetUser(r.Context(), chi.URLParam(r, \"id\"))\n    if err != nil {\n        switch {\n        case errors.Is(err, domain.ErrNotFound):\n            writeError(w, http.StatusNotFound, \"not_found\", err.Error())\n        case errors.Is(err, domain.ErrUnauthorized):\n            writeError(w, http.StatusForbidden, \"forbidden\", \"Access denied\")\n        default:\n            slog.Error(\"unexpected error\", \"err\", err)\n            writeError(w, http.StatusInternalServerError, \"internal_error\", \"An unexpected error occurred\")\n        }\n        return\n    }\n    writeJSON(w, http.StatusOK, user)\n}\n```\n\n## Retry with Exponential Backoff\n\n```typescript\ninterface RetryOptions {\n  maxAttempts?: number\n  baseDelayMs?: number\n  maxDelayMs?: number\n  retryIf?: (error: unknown) => boolean\n}\n\nasync function withRetry\u003CT>(\n  fn: () => Promise\u003CT>,\n  options: RetryOptions = {},\n): Promise\u003CT> {\n  const {\n    maxAttempts = 3,\n    baseDelayMs = 500,\n    maxDelayMs = 10_000,\n    retryIf = () => true,\n  } = options\n\n  let lastError: unknown\n\n  for (let attempt = 1; attempt \u003C= maxAttempts; attempt++) {\n    try {\n      return await fn()\n    } catch (error) {\n      lastError = error\n      if (attempt === maxAttempts || !retryIf(error)) throw error\n\n      const jitter = Math.random() * baseDelayMs\n      const delay = Math.min(baseDelayMs * 2 ** (attempt - 1) + jitter, maxDelayMs)\n      await new Promise(resolve => setTimeout(resolve, delay))\n    }\n  }\n\n  throw lastError\n}\n\n\u002F\u002F Usage: retry transient network errors, not 4xx\nconst data = await withRetry(() => fetch('\u002Fapi\u002Fdata').then(r => r.json()), {\n  maxAttempts: 3,\n  retryIf: (error) => !(error instanceof AppError && error.statusCode \u003C 500),\n})\n```\n\n## User-Facing Error Messages\n\nMap error codes to human-readable messages. Keep technical details out of user-visible text.\n\n```typescript\nconst USER_ERROR_MESSAGES: Record\u003Cstring, string> = {\n  NOT_FOUND: 'The requested item could not be found.',\n  UNAUTHORIZED: 'Please sign in to continue.',\n  FORBIDDEN: \"You don't have permission to do that.\",\n  VALIDATION_ERROR: 'Please check your input and try again.',\n  RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',\n  INTERNAL_ERROR: 'Something went wrong on our end. Please try again later.',\n}\n\nexport function getUserMessage(code: string): string {\n  return USER_ERROR_MESSAGES[code] ?? USER_ERROR_MESSAGES.INTERNAL_ERROR\n}\n```\n\n## Error Handling Checklist\n\nBefore merging any code that touches error handling:\n\n- [ ] Every `catch` block handles, re-throws, or logs — no silent swallowing\n- [ ] API errors follow the standard envelope `{ error: { code, message } }`\n- [ ] User-facing messages contain no stack traces or internal details\n- [ ] Full error context is logged server-side\n- [ ] Custom error classes extend a base `AppError` with a `code` field\n- [ ] Async functions surface errors to callers — no fire-and-forget without fallback\n- [ ] Retry logic only retries retriable errors (not 4xx client errors)\n- [ ] React components are wrapped in `ErrorBoundary` for rendering errors\n","# Error Handling Patterns\n\nConsistent, robust error handling patterns for production applications.\n\n## When to Activate\n\n- Designing error types or exception hierarchies for a new module or service\n- Adding retry logic or circuit breakers for unreliable external dependencies\n- Reviewing API endpoints for missing error handling\n- Implementing user-facing error messages and feedback\n- Debugging cascading failures or silent error swallowing\n\n## Core Principles\n\n1. **Fail fast and loudly** — surface errors at the boundary where they occur; don't bury them\n2. **Typed errors over string messages** — errors are first-class values with structure\n3. **User messages ≠ developer messages** — show friendly text to users, log full context server-side\n4. **Never swallow errors silently** — every `catch` block must either handle, re-throw, or log\n5. **Errors are part of your API contract** — document every error code a client may receive\n\n## TypeScript \u002F JavaScript\n\n### Typed Error Classes\n\n```typescript\n\u002F\u002F Define an error hierarchy for your domain\nexport class AppError extends Error {\n  constructor(\n    message: string,\n    public readonly code: string,\n    public readonly statusCode: number = 500,\n    public readonly details?: unknown,\n  ) {\n    super(message)\n    this.name = this.constructor.name\n    \u002F\u002F Maintain correct prototype chain in transpiled ES5 JavaScript.\n    \u002F\u002F Required for `instanceof` checks (e.g., `error instanceof NotFoundError`)\n    \u002F\u002F to work correctly when extending the built-in Error class.\n    Object.setPrototypeOf(this, new.target.prototype)\n  }\n}\n\nexport class NotFoundError extends AppError {\n  constructor(resource: string, id: string) {\n    super(`${resource} not found: ${id}`, 'NOT_FOUND', 404)\n  }\n}\n\nexport class ValidationError extends AppError {\n  constructor(message: string, details: { field: string; message: string }[]) {\n    super(message, 'VALIDATION_ERROR', 422, details)\n  }\n}\n\nexport class UnauthorizedError extends AppError {\n  constructor(reason = 'Authentication required') {\n    super(reason, 'UNAUTHORIZED', 401)\n  }\n}\n\nexport class RateLimitError extends AppError {\n  constructor(public readonly retryAfterMs: number) {\n    super('Rate limit exceeded', 'RATE_LIMITED', 429)\n  }\n}\n```\n\n### Result Pattern (no-throw style)\n\nFor operations where failure is expected and common (parsing, external calls):\n\n```typescript\ntype Result\u003CT, E = AppError> =\n  | { ok: true; value: T }\n  | { ok: false; error: E }\n\nfunction ok\u003CT>(value: T): Result\u003CT> {\n  return { ok: true, value }\n}\n\nfunction err\u003CE>(error: E): Result\u003Cnever, E> {\n  return { ok: false, error }\n}\n\n\u002F\u002F Usage\nasync function fetchUser(id: string): Promise\u003CResult\u003CUser>> {\n  try {\n    const user = await db.users.findUnique({ where: { id } })\n    if (!user) return err(new NotFoundError('User', id))\n    return ok(user)\n  } catch (e) {\n    return err(new AppError('Database error', 'DB_ERROR'))\n  }\n}\n\nconst result = await fetchUser('abc-123')\nif (!result.ok) {\n  \u002F\u002F TypeScript knows result.error here\n  logger.error('Failed to fetch user', { error: result.error })\n  return\n}\n\u002F\u002F TypeScript knows result.value here\nconsole.log(result.value.email)\n```\n\n### API Error Handler (Next.js \u002F Express)\n\n```typescript\nimport { NextRequest, NextResponse } from 'next\u002Fserver'\n\nfunction handleApiError(error: unknown): NextResponse {\n  \u002F\u002F Known application error\n  if (error instanceof AppError) {\n    return NextResponse.json(\n      {\n        error: {\n          code: error.code,\n          message: error.message,\n          ...(error.details ? { details: error.details } : {}),\n        },\n      },\n      { status: error.statusCode },\n    )\n  }\n\n  \u002F\u002F Zod validation error\n  if (error instanceof z.ZodError) {\n    return NextResponse.json(\n      {\n        error: {\n          code: 'VALIDATION_ERROR',\n          message: 'Request validation failed',\n          details: error.issues.map(i => ({\n            field: i.path.join('.'),\n            message: i.message,\n          })),\n        },\n      },\n      { status: 422 },\n    )\n  }\n\n  \u002F\u002F Unexpected error — log details, return generic message\n  console.error('Unexpected error:', error)\n  return NextResponse.json(\n    { error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } },\n    { status: 500 },\n  )\n}\n\nexport async function POST(req: NextRequest) {\n  try {\n    \u002F\u002F ... handler logic\n  } catch (error) {\n    return handleApiError(error)\n  }\n}\n```\n\n### React Error Boundary\n\n```typescript\nimport { Component, ErrorInfo, ReactNode } from 'react'\n\ninterface Props {\n  fallback: ReactNode\n  onError?: (error: Error, info: ErrorInfo) => void\n  children: ReactNode\n}\n\ninterface State {\n  hasError: boolean\n  error: Error | null\n}\n\nexport class ErrorBoundary extends Component\u003CProps, State> {\n  state: State = { hasError: false, error: null }\n\n  static getDerivedStateFromError(error: Error): State {\n    return { hasError: true, error }\n  }\n\n  componentDidCatch(error: Error, info: ErrorInfo) {\n    this.props.onError?.(error, info)\n    console.error('Unhandled React error:', error, info)\n  }\n\n  render() {\n    if (this.state.hasError) return this.props.fallback\n    return this.props.children\n  }\n}\n\n\u002F\u002F Usage\n\u003CErrorBoundary fallback={\u003Cp>Something went wrong. Please refresh.\u003C\u002Fp>}>\n  \u003CMyComponent \u002F>\n\u003C\u002FErrorBoundary>\n```\n\n## Python\n\n### Custom Exception Hierarchy\n\n```python\nclass AppError(Exception):\n    \"\"\"Base application error.\"\"\"\n    def __init__(self, message: str, code: str, status_code: int = 500):\n        super().__init__(message)\n        self.code = code\n        self.status_code = status_code\n\nclass NotFoundError(AppError):\n    def __init__(self, resource: str, id: str):\n        super().__init__(f\"{resource} not found: {id}\", \"NOT_FOUND\", 404)\n\nclass ValidationError(AppError):\n    def __init__(self, message: str, details: list[dict] | None = None):\n        super().__init__(message, \"VALIDATION_ERROR\", 422)\n        self.details = details or []\n```\n\n### FastAPI Global Exception Handler\n\n```python\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import JSONResponse\n\napp = FastAPI()\n\n@app.exception_handler(AppError)\nasync def app_error_handler(request: Request, exc: AppError) -> JSONResponse:\n    return JSONResponse(\n        status_code=exc.status_code,\n        content={\"error\": {\"code\": exc.code, \"message\": str(exc)}},\n    )\n\n@app.exception_handler(Exception)\nasync def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:\n    # Log full details, return generic message\n    logger.exception(\"Unexpected error\", exc_info=exc)\n    return JSONResponse(\n        status_code=500,\n        content={\"error\": {\"code\": \"INTERNAL_ERROR\", \"message\": \"An unexpected error occurred\"}},\n    )\n```\n\n## Go\n\n### Sentinel Errors and Error Wrapping\n\n```go\npackage domain\n\nimport \"errors\"\n\n\u002F\u002F Sentinel errors for type-checking\nvar (\n    ErrNotFound    = errors.New(\"not found\")\n    ErrUnauthorized = errors.New(\"unauthorized\")\n    ErrConflict     = errors.New(\"conflict\")\n)\n\n\u002F\u002F Wrap errors with context — never lose the original\nfunc (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {\n    user, err := r.db.QueryRow(ctx, \"SELECT * FROM users WHERE id = $1\", id)\n    if errors.Is(err, sql.ErrNoRows) {\n        return nil, fmt.Errorf(\"user %s: %w\", id, ErrNotFound)\n    }\n    if err != nil {\n        return nil, fmt.Errorf(\"querying user %s: %w\", id, err)\n    }\n    return user, nil\n}\n\n\u002F\u002F At the handler level, unwrap to determine response\nfunc (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {\n    user, err := h.service.GetUser(r.Context(), chi.URLParam(r, \"id\"))\n    if err != nil {\n        switch {\n        case errors.Is(err, domain.ErrNotFound):\n            writeError(w, http.StatusNotFound, \"not_found\", err.Error())\n        case errors.Is(err, domain.ErrUnauthorized):\n            writeError(w, http.StatusForbidden, \"forbidden\", \"Access denied\")\n        default:\n            slog.Error(\"unexpected error\", \"err\", err)\n            writeError(w, http.StatusInternalServerError, \"internal_error\", \"An unexpected error occurred\")\n        }\n        return\n    }\n    writeJSON(w, http.StatusOK, user)\n}\n```\n\n## Retry with Exponential Backoff\n\n```typescript\ninterface RetryOptions {\n  maxAttempts?: number\n  baseDelayMs?: number\n  maxDelayMs?: number\n  retryIf?: (error: unknown) => boolean\n}\n\nasync function withRetry\u003CT>(\n  fn: () => Promise\u003CT>,\n  options: RetryOptions = {},\n): Promise\u003CT> {\n  const {\n    maxAttempts = 3,\n    baseDelayMs = 500,\n    maxDelayMs = 10_000,\n    retryIf = () => true,\n  } = options\n\n  let lastError: unknown\n\n  for (let attempt = 1; attempt \u003C= maxAttempts; attempt++) {\n    try {\n      return await fn()\n    } catch (error) {\n      lastError = error\n      if (attempt === maxAttempts || !retryIf(error)) throw error\n\n      const jitter = Math.random() * baseDelayMs\n      const delay = Math.min(baseDelayMs * 2 ** (attempt - 1) + jitter, maxDelayMs)\n      await new Promise(resolve => setTimeout(resolve, delay))\n    }\n  }\n\n  throw lastError\n}\n\n\u002F\u002F Usage: retry transient network errors, not 4xx\nconst data = await withRetry(() => fetch('\u002Fapi\u002Fdata').then(r => r.json()), {\n  maxAttempts: 3,\n  retryIf: (error) => !(error instanceof AppError && error.statusCode \u003C 500),\n})\n```\n\n## User-Facing Error Messages\n\nMap error codes to human-readable messages. Keep technical details out of user-visible text.\n\n```typescript\nconst USER_ERROR_MESSAGES: Record\u003Cstring, string> = {\n  NOT_FOUND: 'The requested item could not be found.',\n  UNAUTHORIZED: 'Please sign in to continue.',\n  FORBIDDEN: \"You don't have permission to do that.\",\n  VALIDATION_ERROR: 'Please check your input and try again.',\n  RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',\n  INTERNAL_ERROR: 'Something went wrong on our end. Please try again later.',\n}\n\nexport function getUserMessage(code: string): string {\n  return USER_ERROR_MESSAGES[code] ?? USER_ERROR_MESSAGES.INTERNAL_ERROR\n}\n```\n\n## Error Handling Checklist\n\nBefore merging any code that touches error handling:\n\n- [ ] Every `catch` block handles, re-throws, or logs — no silent swallowing\n- [ ] API errors follow the standard envelope `{ error: { code, message } }`\n- [ ] User-facing messages contain no stack traces or internal details\n- [ ] Full error context is logged server-side\n- [ ] Custom error classes extend a base `AppError` with a `code` field\n- [ ] Async functions surface errors to callers — no fire-and-forget without fallback\n- [ ] Retry logic only retries retriable errors (not 4xx client errors)\n- [ ] React components are wrapped in `ErrorBoundary` for rendering errors\n",{"name":5,"metadata":23,"description":7},{"origin":24},"ECC","main","1c5cafe28125dcf1bf7940861b865861b99c715e","skills\u002Fecc\u002Ferror-handling",12,"https:\u002F\u002Fgithub.com\u002Fteamai-hub\u002Fteamai-template","https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fteamai-cli","https:\u002F\u002Fgithub.com\u002Fteamai-hub","2026-09-21 11:58:12",{"source":8,"skillName":5,"commands":34,"prompt":44,"zipAvailable":45},[35,38,41],{"tool":36,"command":37},"npx","npx skills add teamai-hub\u002Fteamai-template --skill error-handling",{"tool":39,"command":40},"bunx","bunx skills add teamai-hub\u002Fteamai-template --skill error-handling",{"tool":42,"command":43},"pnpm","pnpm dlx skills add teamai-hub\u002Fteamai-template --skill error-handling","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fteamai-hub\u002Fteamai-template\u002Fblob\u002Fmain\u002Fskills\u002Fecc\u002Ferror-handling\u002FSKILL.md",true,[],[48,55,62,69,76,83],{"skillNo":49,"name":50,"nameZh":6,"description":51,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":52,"categories":53,"keywords":54,"publishedAt":18},"01M79FXCNA4RJN9AWBCQ3CFD2G","tdd","Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions \"red-green-refactor\", or wants integration tests.","https:\u002F\u002Fgithub.com\u002Fteamai-hub\u002Fteamai-template\u002Fblob\u002Fmain\u002Fskills\u002Fmattpocock\u002Ftdd\u002FSKILL.md",[],[],{"skillNo":56,"name":57,"nameZh":6,"description":58,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":59,"categories":60,"keywords":61,"publishedAt":18},"01M79FXAGWJDT4BHKPX6V5AKE2","research","Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent.","https:\u002F\u002Fgithub.com\u002Fteamai-hub\u002Fteamai-template\u002Fblob\u002Fmain\u002Fskills\u002Fmattpocock\u002Fresearch\u002FSKILL.md",[],[],{"skillNo":63,"name":64,"nameZh":6,"description":65,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":66,"categories":67,"keywords":68,"publishedAt":18},"01M79FX8GVJ6MYJ138HQST1YJF","grilling","Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.","https:\u002F\u002Fgithub.com\u002Fteamai-hub\u002Fteamai-template\u002Fblob\u002Fmain\u002Fskills\u002Fmattpocock\u002Fgrilling\u002FSKILL.md",[],[],{"skillNo":70,"name":71,"nameZh":6,"description":72,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":75,"publishedAt":18},"01M79FX7F02MD34KNF6HWA7VN7","grill-me","A relentless interview to sharpen a plan or design.","https:\u002F\u002Fgithub.com\u002Fteamai-hub\u002Fteamai-template\u002Fblob\u002Fmain\u002Fskills\u002Fmattpocock\u002Fgrill-me\u002FSKILL.md",[],[],{"skillNo":77,"name":78,"nameZh":6,"description":79,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":80,"categories":81,"keywords":82,"publishedAt":18},"01M79FX47NFPRZ4HE9VK0YXV1S","domain-modeling","Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model.","https:\u002F\u002Fgithub.com\u002Fteamai-hub\u002Fteamai-template\u002Fblob\u002Fmain\u002Fskills\u002Fmattpocock\u002Fdomain-modeling\u002FSKILL.md",[],[],{"skillNo":84,"name":85,"nameZh":6,"description":86,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":89,"publishedAt":18},"01M79FWZSVFAXDJNJ54436HW7G","tdd-workflow","Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests.","https:\u002F\u002Fgithub.com\u002Fteamai-hub\u002Fteamai-template\u002Fblob\u002Fmain\u002Fskills\u002Fecc\u002Ftdd-workflow\u002FSKILL.md",[],[]]