> For the complete Lynkow documentation index in agent-friendly format, see [llms.txt](/llms.txt).

# Error Handling & Resilience

**Publié le** : 2026-08-14
**Catégorie** : Advanced

Handle every failure mode from the Lynkow SDK gracefully -- show the right UI, retry when appropriate, and log what matters.

> **Reference** - This guide is a tutorial. For exhaustive, always-current signatures and field lists, see the auto-generated reference:
> 
> - SDK: [Lynkow SDK](/docs/sdk)
> - Types: [SDK Types](/docs/sdk/types)
> - API: [Storefront API](/docs/storefront-api/overview)

---

## Prerequisites

- `lynkow` SDK installed and configured ([Quick Start](/docs/sdk/getting-started/quick-start))
- Next.js 15 App Router project with TypeScript
- Basic familiarity with React Error Boundaries

---

## 1. The LynkowError Class

Every error thrown by the SDK is a `LynkowError` instance. It extends the native `Error` with structured fields. The fields the patterns in this guide read:

```typescript
import { LynkowError, isLynkowError } from 'lynkow'

interface LynkowError extends Error {
  code: ErrorCode              // machine-readable classification, e.g. 'NOT_FOUND'
  status?: number              // HTTP status (undefined for TIMEOUT / NETWORK_ERROR)
  details?: ApiErrorDetail[]   // field-level errors (VALIDATION_ERROR only)
  cause?: Error                // original error (AbortError, TypeError, and so on)
  toJSON(): Record<string, unknown>
}
```

See the full `LynkowError`, `ApiErrorDetail`, and `ErrorCode` shapes in the [SDK Types reference](/docs/sdk/types). Every error code with its HTTP status and recommended action is listed in the [Error Code Reference](#3-error-code-reference) table below.

**Key points:**

- `code` is always present and is a finite set of strings -- use it for branching logic instead of checking `status`.
- `status` is the HTTP status code from the API. It is `undefined` for client-side failures (`TIMEOUT`, `NETWORK_ERROR`) where no HTTP response was received.
- `details` is only populated for `VALIDATION_ERROR`. Each entry has a `message` and optionally a `field` name that maps to your form input.
- `cause` preserves the original error: an `AbortError` for timeouts, a `TypeError` for network failures. Useful for debugging.
- `toJSON()` produces a plain object suitable for structured logging.

---

## 2. Using the isLynkowError Type Guard

The `isLynkowError` function narrows `unknown` to `LynkowError`. Always use it in catch blocks since you cannot know what was thrown:

```typescript
import { isLynkowError } from 'lynkow'

try {
  const article = await lynkow.contents.getBySlug('my-article')
} catch (error: unknown) {
  if (isLynkowError(error)) {
    // TypeScript knows error is LynkowError here
    console.log(error.code)    // 'NOT_FOUND'
    console.log(error.status)  // 404
    console.log(error.details) // undefined (not a validation error)
  } else {
    // Not from the SDK -- a programming error, JSON parse failure, etc.
    console.error('Unexpected error:', error)
  }
}
```

---

## 3. Error Code Reference

| Code | HTTP Status | When It Occurs | Recommended Action |
| --- | --- | --- | --- |
| `BAD_REQUEST` | 400 | Malformed request parameters | Fix the request -- check parameter types and formats |
| `VALIDATION_ERROR` | 422 | Invalid field values (e.g., form submission) | Display `details[].message` next to corresponding fields |
| `UNAUTHORIZED` | 401 | Invalid or missing `siteId` / API key | Check SDK configuration -- likely a wrong `siteId` |
| `FORBIDDEN` | 403 | Valid auth but insufficient permissions | The resource exists but is not accessible publicly |
| `NOT_FOUND` | 404 | Resource does not exist or is not published | Show 404 page or fallback content |
| `RATE_LIMITED` | 429 | Too many requests in a short period | Retry with exponential backoff |
| `TOO_MANY_REQUESTS` | 429 | Sustained rate limit violation | Back off longer, consider caching |
| `TIMEOUT` | *undefined* | Request exceeded time limit (no response received) | Retry or show fallback UI with retry button |
| `NETWORK_ERROR` | *undefined* | DNS failure, offline, connection refused | Show offline indicator, retry when connectivity returns |
| `INTERNAL_ERROR` | 500 | Server-side bug or crash | Log for monitoring, show generic error page |
| `SERVICE_UNAVAILABLE` | 503 | API is down for maintenance or overloaded | Show maintenance page, retry after delay |
| `UNKNOWN` | varies | Unclassified error | Log the full error for investigation |


---

## 4. Pattern: NOT_FOUND -- Trigger Next.js 404

When a content page receives `NOT_FOUND`, call `notFound()` from `next/navigation` to render the closest `not-found.tsx`:

```typescript
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { lynkow, isLynkowError } from '@/lib/lynkow'

interface Props {
  params: Promise<{ slug: string }>
}

export default async function ArticlePage({ params }: Props) {
  const { slug } = await params

  try {
    const article = await lynkow.contents.getBySlug(slug)

    return (
      <article>
        <h1>{article.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: article.html }} />
      </article>
    )
  } catch (error: unknown) {
    if (isLynkowError(error) && error.code === 'NOT_FOUND') {
      notFound()
    }
    throw error // re-throw everything else to the error boundary
  }
}
```

Create the matching `not-found.tsx` (see section 13 for the full implementation).

---

## 5. Pattern: VALIDATION_ERROR -- Display Field Errors Inline

When submitting a form (e.g., contact form, review), map `details` back to form fields:

```typescript
// app/contact/contact-form.tsx
'use client'

import { useState } from 'react'
import { lynkow, isLynkowError, type LynkowError } from 'lynkow'

interface FieldErrors {
  [field: string]: string
}

export function ContactForm() {
  const [fieldErrors, setFieldErrors] = useState<FieldErrors>({})
  const [globalError, setGlobalError] = useState<string | null>(null)

  async function handleSubmit(formData: FormData) {
    setFieldErrors({})
    setGlobalError(null)

    try {
      await lynkow.forms.submit('contact', {
        name: formData.get('name') as string,
        email: formData.get('email') as string,
        message: formData.get('message') as string,
      })
    } catch (error: unknown) {
      if (isLynkowError(error) && error.code === 'VALIDATION_ERROR' && error.details) {
        // Map each detail to its field
        const errors: FieldErrors = {}
        for (const detail of error.details) {
          if (detail.field) {
            errors[detail.field] = detail.message
          }
        }
        setFieldErrors(errors)

        // Collect any errors without a field for a global message
        const globalErrors = error.details
          .filter((d) => !d.field)
          .map((d) => d.message)
        if (globalErrors.length > 0) {
          setGlobalError(globalErrors.join('. '))
        }
        return
      }

      setGlobalError('Something went wrong. Please try again.')
    }
  }

  return (
    <form action={handleSubmit}>
      {globalError && (
        <div role="alert" className="mb-4 rounded bg-red-50 p-3 text-sm text-red-700">
          {globalError}
        </div>
      )}

      <div className="mb-4">
        <label htmlFor="name" className="block text-sm font-medium">
          Name
        </label>
        <input
          id="name"
          name="name"
          className={`mt-1 block w-full rounded border px-3 py-2 ${
            fieldErrors.name ? 'border-red-500' : 'border-gray-300'
          }`}
        />
        {fieldErrors.name && (
          <p className="mt-1 text-sm text-red-600">{fieldErrors.name}</p>
        )}
      </div>

      <div className="mb-4">
        <label htmlFor="email" className="block text-sm font-medium">
          Email
        </label>
        <input
          id="email"
          name="email"
          type="email"
          className={`mt-1 block w-full rounded border px-3 py-2 ${
            fieldErrors.email ? 'border-red-500' : 'border-gray-300'
          }`}
        />
        {fieldErrors.email && (
          <p className="mt-1 text-sm text-red-600">{fieldErrors.email}</p>
        )}
      </div>

      <div className="mb-4">
        <label htmlFor="message" className="block text-sm font-medium">
          Message
        </label>
        <textarea
          id="message"
          name="message"
          rows={4}
          className={`mt-1 block w-full rounded border px-3 py-2 ${
            fieldErrors.message ? 'border-red-500' : 'border-gray-300'
          }`}
        />
        {fieldErrors.message && (
          <p className="mt-1 text-sm text-red-600">{fieldErrors.message}</p>
        )}
      </div>

      <button
        type="submit"
        className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
      >
        Send
      </button>
    </form>
  )
}
```

---

## 6. Pattern: RATE_LIMITED -- Exponential Backoff Retry

> **The SDK does this for you since v1.41.** Read requests (GET and text endpoints like the sitemap) are automatically retried on 429, 503, and network errors with exponential backoff, honoring the server's `Retry-After` header. Form and review submissions (POST) are never auto-retried. You only need the manual pattern below if you disabled retries (`retry: false` in `createClient`) or need behavior beyond `retry: { retries, backoffMs, maxDelayMs }`.

When the API returns 429, the SDK throws with `code: 'RATE_LIMITED'`. Don't retry immediately -- wait with exponential backoff:

```typescript
import { isLynkowError } from 'lynkow'

async function fetchWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelayMs = 1000
): Promise<T> {
  let attempt = 0

  while (true) {
    try {
      return await fn()
    } catch (error: unknown) {
      attempt++
      if (
        isLynkowError(error) &&
        (error.code === 'RATE_LIMITED' || error.code === 'TOO_MANY_REQUESTS') &&
        attempt <= maxRetries
      ) {
        const delay = baseDelayMs * Math.pow(2, attempt - 1)
        const jitter = Math.random() * baseDelayMs * 0.5
        await new Promise((resolve) => setTimeout(resolve, delay + jitter))
        continue
      }
      throw error
    }
  }
}

// Usage
const articles = await fetchWithBackoff(() => lynkow.contents.list({ limit: 20 }))
```

---

## 7. Pattern: NETWORK_ERROR / TIMEOUT -- Fallback UI with Retry

On the client side, network errors mean the user is offline or the API is unreachable. Show a fallback with a retry button:

```typescript
// app/blog/article-loader.tsx
'use client'

import { useState, useEffect, useCallback } from 'react'
import { lynkow, isLynkowError, type Content } from 'lynkow'

interface Props {
  slug: string
}

export function ArticleLoader({ slug }: Props) {
  const [article, setArticle] = useState<Content | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [isRetryable, setIsRetryable] = useState(false)
  const [loading, setLoading] = useState(true)

  const fetchArticle = useCallback(async () => {
    setLoading(true)
    setError(null)
    setIsRetryable(false)

    try {
      const data = await lynkow.contents.getBySlug(slug)
      setArticle(data)
    } catch (err: unknown) {
      if (isLynkowError(err)) {
        switch (err.code) {
          case 'NETWORK_ERROR':
            setError('Unable to connect. Check your internet connection and try again.')
            setIsRetryable(true)
            break
          case 'TIMEOUT':
            setError('The request timed out. Please try again.')
            setIsRetryable(true)
            break
          case 'SERVICE_UNAVAILABLE':
            setError('The service is temporarily unavailable. Please try again in a moment.')
            setIsRetryable(true)
            break
          case 'NOT_FOUND':
            setError('This article could not be found.')
            setIsRetryable(false)
            break
          default:
            setError('Something went wrong. Please try again later.')
            setIsRetryable(true)
        }
      } else {
        setError('An unexpected error occurred.')
        setIsRetryable(false)
      }
    } finally {
      setLoading(false)
    }
  }, [slug])

  useEffect(() => {
    fetchArticle()
  }, [fetchArticle])

  if (loading) {
    return <div className="animate-pulse h-64 rounded bg-gray-100" />
  }

  if (error) {
    return (
      <div className="rounded border border-red-200 bg-red-50 p-6 text-center">
        <p className="mb-4 text-red-700">{error}</p>
        {isRetryable && (
          <button
            onClick={fetchArticle}
            className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
          >
            Try Again
          </button>
        )}
      </div>
    )
  }

  if (!article) return null

  return (
    <article>
      <h1 className="text-3xl font-bold">{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.html }} />
    </article>
  )
}
```

---

## 8. Pattern: UNAUTHORIZED -- Configuration Error

A `401 Unauthorized` from the public SDK almost always means the `siteId` is wrong or missing. The public SDK does not use user credentials -- only a site identifier:

```typescript
import { isLynkowError } from 'lynkow'

try {
  const articles = await lynkow.contents.list()
} catch (error: unknown) {
  if (isLynkowError(error) && error.code === 'UNAUTHORIZED') {
    // This is a configuration problem, not a user problem.
    // In development, throw a clear message:
    if (process.env.NODE_ENV === 'development') {
      throw new Error(
        `Lynkow UNAUTHORIZED: Check that NEXT_PUBLIC_LYNKOW_SITE_ID is set correctly. ` +
        `Current value: "${process.env.NEXT_PUBLIC_LYNKOW_SITE_ID}". ` +
        `Original error: ${error.message}`
      )
    }
    // In production, log and show a generic error
    console.error('[Lynkow] Configuration error: invalid siteId', error.toJSON())
    throw error
  }
  throw error
}
```

> **Tip:** If you receive `FORBIDDEN` (403) instead, the site exists but the requested resource may require authentication via the V1 admin API (e.g., draft content). See the [Draft Preview guide](/docs/sdk/advanced/draft-preview).

---

## 9. Building a withRetry Utility

A generic retry wrapper that handles both rate limiting and transient network errors:

```typescript
// lib/with-retry.ts
import { isLynkowError } from 'lynkow'

interface RetryOptions {
  /** Maximum number of retry attempts (default: 3) */
  maxRetries?: number
  /** Base delay in milliseconds before first retry (default: 1000) */
  baseDelayMs?: number
  /** Maximum delay cap in milliseconds (default: 30000) */
  maxDelayMs?: number
  /** Error codes that should trigger a retry */
  retryOn?: string[]
}

const DEFAULT_RETRYABLE_CODES = [
  'RATE_LIMITED',
  'TOO_MANY_REQUESTS',
  'TIMEOUT',
  'NETWORK_ERROR',
  'SERVICE_UNAVAILABLE',
]

export async function withRetry<T>(
  fn: () => Promise<T>,
  options: RetryOptions = {}
): Promise<T> {
  const {
    maxRetries = 3,
    baseDelayMs = 1000,
    maxDelayMs = 30_000,
    retryOn = DEFAULT_RETRYABLE_CODES,
  } = options

  let lastError: unknown

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error: unknown) {
      lastError = error

      // Only retry LynkowErrors with retryable codes
      if (!isLynkowError(error) || !retryOn.includes(error.code)) {
        throw error
      }

      // Don't wait after the last attempt
      if (attempt === maxRetries) {
        break
      }

      // Exponential backoff with jitter
      const exponentialDelay = baseDelayMs * Math.pow(2, attempt)
      const jitter = Math.random() * baseDelayMs
      const delay = Math.min(exponentialDelay + jitter, maxDelayMs)

      await new Promise((resolve) => setTimeout(resolve, delay))
    }
  }

  throw lastError
}
```

Usage in server components and data fetching:

```typescript
// app/blog/page.tsx
import { lynkow } from '@/lib/lynkow'
import { withRetry } from '@/lib/with-retry'

export default async function BlogPage() {
  const articles = await withRetry(
    () => lynkow.contents.list({ limit: 20 }),
    { maxRetries: 2, baseDelayMs: 500 }
  )

  return (
    <ul>
      {articles.data.map((article) => (
        <li key={article.id}>{article.title}</li>
      ))}
    </ul>
  )
}
```

---

## 10. React Error Boundary for Graceful Degradation

Error boundaries catch rendering errors in their subtree. Combine them with `isLynkowError` to show context-appropriate fallbacks:

```typescript
// components/error-boundary.tsx
'use client'

import { Component, type ReactNode } from 'react'
import { isLynkowError } from 'lynkow'

interface Props {
  children: ReactNode
  /** Rendered when a non-retryable error occurs */
  fallback?: ReactNode
}

interface State {
  error: unknown
  hasError: boolean
}

export class LynkowErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { error: null, hasError: false }
  }

  static getDerivedStateFromError(error: unknown): State {
    return { error, hasError: true }
  }

  componentDidCatch(error: unknown) {
    if (isLynkowError(error)) {
      console.error(
        `[LynkowError] code=${error.code} status=${error.status}`,
        error.toJSON()
      )
    }
  }

  private reset = () => {
    this.setState({ error: null, hasError: false })
  }

  render() {
    if (!this.state.hasError) {
      return this.props.children
    }

    const { error } = this.state

    if (isLynkowError(error)) {
      switch (error.code) {
        case 'NETWORK_ERROR':
        case 'TIMEOUT':
        case 'SERVICE_UNAVAILABLE':
          return (
            <div className="rounded border border-amber-200 bg-amber-50 p-6 text-center">
              <h2 className="mb-2 text-lg font-semibold text-amber-800">
                Connection Issue
              </h2>
              <p className="mb-4 text-amber-700">
                We couldn&apos;t load this content. Please check your connection.
              </p>
              <button
                onClick={this.reset}
                className="rounded bg-amber-600 px-4 py-2 text-white hover:bg-amber-700"
              >
                Retry
              </button>
            </div>
          )

        case 'NOT_FOUND':
          return (
            <div className="rounded border border-gray-200 bg-gray-50 p-6 text-center">
              <h2 className="text-lg font-semibold text-gray-600">Not Found</h2>
              <p className="text-gray-500">This content is no longer available.</p>
            </div>
          )

        default:
          // INTERNAL_ERROR, UNKNOWN, etc.
          return (
            <div className="rounded border border-red-200 bg-red-50 p-6 text-center">
              <h2 className="mb-2 text-lg font-semibold text-red-800">
                Something went wrong
              </h2>
              <p className="mb-4 text-sm text-red-600">
                Error code: {error.code}
              </p>
              <button
                onClick={this.reset}
                className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
              >
                Try Again
              </button>
            </div>
          )
      }
    }

    // Non-Lynkow error -- use the provided fallback or a generic message
    return (
      this.props.fallback ?? (
        <div className="rounded border border-red-200 bg-red-50 p-6 text-center">
          <p className="text-red-700">An unexpected error occurred.</p>
        </div>
      )
    )
  }
}
```

Usage:

```tsx
// app/blog/[slug]/page.tsx (layout wrapping)
import { LynkowErrorBoundary } from '@/components/error-boundary'
import { ArticleLoader } from './article-loader'

export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params

  return (
    <LynkowErrorBoundary>
      <ArticleLoader slug={slug} />
    </LynkowErrorBoundary>
  )
}
```

---

## 11. Logging to External Services

Use `error.toJSON()` to send structured error data to your error tracking service. The example below uses Sentry, but the same pattern works with Datadog, Bugsnag, Rollbar, LogRocket, or any other APM that accepts a serializable payload , swap the import and the capture call, the shape of `toJSON()` stays the same:

```typescript
// lib/error-logging.ts
import * as Sentry from '@sentry/nextjs'
import { isLynkowError } from 'lynkow'

export function captureError(error: unknown, context?: Record<string, unknown>) {
  if (isLynkowError(error)) {
    Sentry.captureException(error, {
      tags: {
        'lynkow.code': error.code,
        'lynkow.status': error.status?.toString() ?? 'none',
      },
      extra: {
        ...error.toJSON(),
        ...context,
      },
      // Don't flood Sentry with rate limit errors
      level: error.code === 'RATE_LIMITED' ? 'warning' : 'error',
    })
  } else {
    Sentry.captureException(error, { extra: context })
  }
}
```

Integrate it into your data fetching:

```typescript
// lib/lynkow-fetch.ts
import { lynkow, isLynkowError } from '@/lib/lynkow'
import { withRetry } from '@/lib/with-retry'
import { captureError } from '@/lib/error-logging'

export async function fetchContent(slug: string) {
  try {
    return await withRetry(() => lynkow.contents.getBySlug(slug))
  } catch (error: unknown) {
    captureError(error, { slug })
    throw error
  }
}
```

---

## 12. Server Component vs Client Component Error Handling

Error handling differs significantly between server and client components in Next.js.

### Server Components

Server components run on the server during rendering. Errors propagate to the nearest `error.tsx` boundary:

```typescript
// app/blog/[slug]/page.tsx (server component)
import { notFound } from 'next/navigation'
import { lynkow, isLynkowError } from '@/lib/lynkow'
import { captureError } from '@/lib/error-logging'

interface Props {
  params: Promise<{ slug: string }>
}

export default async function ArticlePage({ params }: Props) {
  const { slug } = await params

  try {
    const article = await lynkow.contents.getBySlug(slug)

    return (
      <article className="prose mx-auto max-w-3xl py-12">
        <h1>{article.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: article.html }} />
      </article>
    )
  } catch (error: unknown) {
    if (isLynkowError(error)) {
      if (error.code === 'NOT_FOUND') {
        notFound() // renders not-found.tsx
      }
      // Log non-404 errors
      captureError(error, { slug })
    }
    throw error // propagates to error.tsx
  }
}
```

**Key behaviors:**

- `notFound()` renders the nearest `not-found.tsx` -- it does not throw.
- Thrown errors are caught by the nearest `error.tsx` in the route hierarchy.
- Server components cannot use `useState` or retry buttons -- delegate interactive error recovery to a client component via `error.tsx`.

### Client Components

Client components handle errors locally with try/catch and state:

```typescript
// components/latest-articles.tsx
'use client'

import { useState, useEffect } from 'react'
import { lynkow, isLynkowError, type Content } from 'lynkow'

export function LatestArticles() {
  const [articles, setArticles] = useState<Content[]>([])
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(true)

  async function load() {
    setLoading(true)
    setError(null)
    try {
      const response = await lynkow.contents.list({ limit: 5 })
      setArticles(response.data)
    } catch (err: unknown) {
      if (isLynkowError(err) && err.code === 'NETWORK_ERROR') {
        setError('You appear to be offline.')
      } else {
        setError('Failed to load articles.')
      }
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => { load() }, [])

  if (loading) return <p>Loading...</p>
  if (error) {
    return (
      <div>
        <p>{error}</p>
        <button onClick={load}>Retry</button>
      </div>
    )
  }

  return (
    <ul>
      {articles.map((a) => (
        <li key={a.id}>{a.title}</li>
      ))}
    </ul>
  )
}
```

**Key behaviors:**

- Errors are caught locally -- they do not bubble to `error.tsx`.
- You can offer retry buttons immediately.
- Use `isLynkowError` to give specific messages for different failure modes.

---

## 13. Complete error.tsx and not-found.tsx

### error.tsx

This file catches all unhandled errors in its route segment. It must be a client component:

```typescript
// app/error.tsx
'use client'

import { useEffect } from 'react'
import { isLynkowError } from 'lynkow'
import { captureError } from '@/lib/error-logging'

interface Props {
  error: Error & { digest?: string }
  reset: () => void
}

export default function GlobalError({ error, reset }: Props) {
  useEffect(() => {
    captureError(error)
  }, [error])

  // Determine the user-facing message based on the error type
  let title = 'Something went wrong'
  let description = 'An unexpected error occurred. Please try again.'
  let showRetry = true

  if (isLynkowError(error)) {
    switch (error.code) {
      case 'NETWORK_ERROR':
        title = 'Connection lost'
        description =
          'We could not reach the server. Please check your internet connection and try again.'
        break

      case 'TIMEOUT':
        title = 'Request timed out'
        description = 'The server took too long to respond. Please try again.'
        break

      case 'SERVICE_UNAVAILABLE':
        title = 'Service unavailable'
        description =
          'Our servers are temporarily down for maintenance. Please try again in a few minutes.'
        break

      case 'RATE_LIMITED':
      case 'TOO_MANY_REQUESTS':
        title = 'Too many requests'
        description = 'You are sending requests too quickly. Please wait a moment and try again.'
        break

      case 'UNAUTHORIZED':
        title = 'Configuration error'
        description =
          'The site could not be authenticated. If you are the site owner, check your Lynkow configuration.'
        showRetry = false
        break

      case 'INTERNAL_ERROR':
        title = 'Server error'
        description = 'Something went wrong on our end. Our team has been notified.'
        break
    }
  }

  return (
    <div className="flex min-h-[50vh] flex-col items-center justify-center px-4">
      <div className="max-w-md text-center">
        <h1 className="mb-4 text-2xl font-bold text-gray-900">{title}</h1>
        <p className="mb-8 text-gray-600">{description}</p>

        {error.digest && (
          <p className="mb-4 text-xs text-gray-400">Error ID: {error.digest}</p>
        )}

        <div className="flex justify-center gap-4">
          {showRetry && (
            <button
              onClick={reset}
              className="rounded bg-blue-600 px-6 py-2.5 text-sm font-medium text-white hover:bg-blue-700"
            >
              Try again
            </button>
          )}
          <a
            href="/"
            className="rounded border border-gray-300 px-6 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50"
          >
            Go home
          </a>
        </div>
      </div>
    </div>
  )
}
```

### not-found.tsx

This renders when `notFound()` is called or when a route does not match:

```typescript
// app/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="flex min-h-[50vh] flex-col items-center justify-center px-4">
      <div className="max-w-md text-center">
        <p className="mb-2 text-6xl font-bold text-gray-300">404</p>
        <h1 className="mb-4 text-2xl font-bold text-gray-900">Page not found</h1>
        <p className="mb-8 text-gray-600">
          The page you are looking for does not exist or has been moved.
        </p>
        <div className="flex justify-center gap-4">
          <Link
            href="/"
            className="rounded bg-blue-600 px-6 py-2.5 text-sm font-medium text-white hover:bg-blue-700"
          >
            Go home
          </Link>
          <Link
            href="/blog"
            className="rounded border border-gray-300 px-6 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50"
          >
            Browse articles
          </Link>
        </div>
      </div>
    </div>
  )
}
```

### Route-specific error.tsx

You can also create error boundaries scoped to specific route segments. For example, a blog-specific error page:

```typescript
// app/blog/error.tsx
'use client'

import { useEffect } from 'react'
import { isLynkowError } from 'lynkow'
import { captureError } from '@/lib/error-logging'

interface Props {
  error: Error & { digest?: string }
  reset: () => void
}

export default function BlogError({ error, reset }: Props) {
  useEffect(() => {
    captureError(error, { section: 'blog' })
  }, [error])

  const isTransient =
    isLynkowError(error) &&
    ['NETWORK_ERROR', 'TIMEOUT', 'SERVICE_UNAVAILABLE', 'RATE_LIMITED'].includes(error.code)

  return (
    <div className="mx-auto max-w-2xl py-16 text-center">
      <h2 className="mb-4 text-xl font-semibold text-gray-900">
        {isTransient ? 'Temporarily unavailable' : 'Error loading blog'}
      </h2>
      <p className="mb-6 text-gray-600">
        {isTransient
          ? 'The blog is temporarily unreachable. This usually resolves quickly.'
          : 'We encountered an error loading this page.'}
      </p>
      <button
        onClick={reset}
        className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
      >
        Try again
      </button>
    </div>
  )
}
```

### Route-specific not-found.tsx

```typescript
// app/blog/[slug]/not-found.tsx
import Link from 'next/link'

export default function ArticleNotFound() {
  return (
    <div className="mx-auto max-w-2xl py-16 text-center">
      <h2 className="mb-4 text-xl font-semibold text-gray-900">
        Article not found
      </h2>
      <p className="mb-6 text-gray-600">
        This article may have been removed or its URL may have changed.
      </p>
      <Link
        href="/blog"
        className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
      >
        Browse all articles
      </Link>
    </div>
  )
}
```

---

## Summary

| Scenario | Strategy |
| --- | --- |
| `NOT_FOUND` | Call `notFound()` in server components |
| `VALIDATION_ERROR` | Map `details[].field` to form inputs |
| `RATE_LIMITED` / `TOO_MANY_REQUESTS` | Exponential backoff with `withRetry()` |
| `NETWORK_ERROR` / `TIMEOUT` | Show fallback UI with retry button |
| `UNAUTHORIZED` | Fix `siteId` configuration |
| `SERVICE_UNAVAILABLE` | Retry after delay, show maintenance message |
| `INTERNAL_ERROR` | Log to your error tracking service, show generic error page |
| Any error in server component | Caught by nearest `error.tsx` |
| Any error in client component | Handle locally with try/catch + state |