Handle every failure mode from the Lynkow SDK gracefully -- show the right UI, retry when appropriate, and log what matters.
Prerequisites
lynkowSDK installed and configured (Getting Started guide)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:
import { LynkowError, isLynkowError } from 'lynkow'
// LynkowError extends Error
interface LynkowError extends Error {
/** Machine-readable error classification */
code: ErrorCode
/** HTTP status code (undefined for TIMEOUT and NETWORK_ERROR) */
status?: number
/** Field-level validation errors (only present for VALIDATION_ERROR) */
details?: ApiErrorDetail[]
/** The original underlying error (AbortError for TIMEOUT, TypeError for NETWORK_ERROR) */
cause?: Error
/** Serialize for logging / external services */
toJSON(): Record<string, unknown>
}
interface ApiErrorDetail {
message: string
field?: string
code?: string
}
type ErrorCode =
| 'BAD_REQUEST'
| 'VALIDATION_ERROR'
| 'UNAUTHORIZED'
| 'FORBIDDEN'
| 'NOT_FOUND'
| 'RATE_LIMITED'
| 'TOO_MANY_REQUESTS'
| 'TIMEOUT'
| 'NETWORK_ERROR'
| 'INTERNAL_ERROR'
| 'SERVICE_UNAVAILABLE'
| 'UNKNOWN'Key points:
codeis always present and is a finite set of strings -- use it for branching logic instead of checkingstatus.statusis the HTTP status code from the API. It isundefinedfor client-side failures (TIMEOUT,NETWORK_ERROR) where no HTTP response was received.detailsis only populated forVALIDATION_ERROR. Each entry has amessageand optionally afieldname that maps to your form input.causepreserves the original error: anAbortErrorfor timeouts, aTypeErrorfor 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:
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 |
|---|---|---|---|
| 400 | Malformed request parameters | Fix the request -- check parameter types and formats |
| 422 | Invalid field values (e.g., form submission) | Display |
| 401 | Invalid or missing | Check SDK configuration -- likely a wrong |
| 403 | Valid auth but insufficient permissions | The resource exists but is not accessible publicly |
| 404 | Resource does not exist or is not published | Show 404 page or fallback content |
| 429 | Too many requests in a short period | Retry with exponential backoff |
| 429 | Sustained rate limit violation | Back off longer, consider caching |
| undefined | Request exceeded time limit (no response received) | Retry or show fallback UI with retry button |
| undefined | DNS failure, offline, connection refused | Show offline indicator, retry when connectivity returns |
| 500 | Server-side bug or crash | Log for monitoring, show generic error page |
| 503 | API is down for maintenance or overloaded | Show maintenance page, retry after delay |
| 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:
// 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:
// 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
When the API returns 429, the SDK throws with code: 'RATE_LIMITED'. Don't retry immediately -- wait with exponential backoff:
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:
// 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:
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.
9. Building a withRetry Utility
A generic retry wrapper that handles both rate limiting and transient network errors:
// 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:
// 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:
// 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'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:
// 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 monitoring services like Sentry:
// 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:
// 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:
// 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 nearestnot-found.tsx-- it does not throw.Thrown errors are caught by the nearest
error.tsxin the route hierarchy.Server components cannot use
useStateor retry buttons -- delegate interactive error recovery to a client component viaerror.tsx.
Client Components
Client components handle errors locally with try/catch and state:
// 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
isLynkowErrorto 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:
// 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:
// 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:
// 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
// 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 |
|---|---|
| Call |
| Map |
| Exponential backoff with |
| Show fallback UI with retry button |
| Fix |
| Retry after delay, show maintenance message |
| Log to Sentry, show generic error page |
Any error in server component | Caught by nearest |
Any error in client component | Handle locally with try/catch + state |