Error Handling
Implement robust error handling to prevent crashes and provide excellent user experience.
Overview
Strapi-NextGen Framework provides multiple layers of error handling:
- ✅ Error Boundaries - Catch React component errors
- ✅ Graceful Degradation - Components fail safely
- ✅ Validation - Zod schema validation
- ✅ Null Safety - TypeScript optional chaining
- ✅ Logging - Development error details
Error Handling Layers
1. Network Errors (Data Layer)
Handle Strapi connection issues:
app/page.tsx
import { strapi } from '@/lib/strapi';
import { GetPageDocument } from '@/graphql/generated';
export default async function Page() {
try {
const data = await strapi.getPage('home', GetPageDocument);
const page = data.page?.data?.attributes;
if (!page) {
return (
<div className="container mx-auto px-4 py-16">
<h1>Page not found</h1>
<p>The page you're looking for doesn't exist.</p>
</div>
);
}
return (
<main>
<h1>{page.title}</h1>
{/* Your content */}
</main>
);
} catch (error) {
console.error('Failed to fetch page:', error);
return (
<div className="container mx-auto px-4 py-16">
<h1>Something went wrong</h1>
<p>We're having trouble loading this page. Please try again later.</p>
</div>
);
}
}2. Component Errors (Presentation Layer)
Use built-in error boundaries:
import { StrapiRenderer } from 'strapi-nextgen-framework';
<StrapiRenderer
sections={page.sections}
componentMap={componentMap}
fallback={(typename) => (
<div className="error-fallback">
<p>Unable to render section: {typename}</p>
</div>
)}
/>Behavior:
- Each component is wrapped in an error boundary
- If one component crashes, others continue rendering
- Development shows detailed error
- Production shows fallback UI
3. Data Validation
Validate data with Zod:
lib/validators.ts
import { z } from 'zod';
export const ArticleSchema = z.object({
title: z.string().min(1, 'Title is required'),
content: z.string().min(10, 'Content must be at least 10 characters'),
publishedAt: z.string().datetime().optional(),
author: z.object({
name: z.string(),
}).optional(),
});
export function validateArticle(data: unknown) {
const result = ArticleSchema.safeParse(data);
if (!result.success) {
console.error('Validation errors:', result.error.errors);
return null;
}
return result.data;
}app/blog/[slug]/page.tsx
import { validateArticle } from '@/lib/validators';
export default async function Page({ params }: { params: { slug: string } }) {
const data = await strapi.getCollection('articles', GetArticlesDocument, {
filters: { slug: { eq: params.slug } },
});
const article = data.articles?.data[0]?.attributes;
const validated = validateArticle(article);
if (!validated) {
return <div>Invalid article data</div>;
}
return (
<article>
<h1>{validated.title}</h1>
<div>{validated.content}</div>
</article>
);
}Built-in Error Boundaries
StrapiRenderer Error Boundary
Automatic for dynamic sections:
import { StrapiRenderer } from 'strapi-nextgen-framework';
<StrapiRenderer
sections={sections}
componentMap={componentMap}
fallback={(typename, error) => {
// Log to error service
console.error('Section error:', typename, error);
// Return user-friendly UI
return (
<div className="section-error">
<p>This section is temporarily unavailable.</p>
</div>
);
}}
/>Custom Error Boundary
For specific components:
components/ErrorBoundary.tsx
'use client';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-boundary">
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.message}</pre>
</details>
</div>
);
}
return this.props.children;
}
}Usage:
<ErrorBoundary fallback={<ErrorMessage />}>
<ComplexComponent data={data} />
</ErrorBoundary>Error Pages
Not Found (404)
app/blog/[slug]/not-found.tsx
export default function NotFound() {
return (
<div className="container mx-auto px-4 py-16 text-center">
<h1 className="text-4xl font-bold mb-4">404 - Article Not Found</h1>
<p className="text-gray-600 mb-8">
The article you're looking for doesn't exist.
</p>
<a href="/blog" className="btn btn-primary">
Back to Blog
</a>
</div>
);
}app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: { slug: string } }) {
const data = await strapi.getCollection('articles', GetArticlesDocument, {
filters: { slug: { eq: params.slug } },
});
if (!data.articles?.data[0]) {
notFound(); // Shows not-found.tsx
}
// Render article...
}Error Page (500)
app/blog/[slug]/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="container mx-auto px-4 py-16">
<h1 className="text-4xl font-bold mb-4">Something went wrong!</h1>
<p className="text-gray-600 mb-8">
We encountered an error while loading this article.
</p>
{process.env.NODE_ENV === 'development' && (
<details className="mb-8">
<summary>Error details (development only)</summary>
<pre className="bg-red-50 p-4 rounded overflow-auto">
{error.message}
</pre>
</details>
)}
<button onClick={reset} className="btn btn-primary">
Try again
</button>
</div>
);
}Global Error
app/global-error.tsx
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-6xl font-bold mb-4">Oops!</h1>
<p className="text-xl mb-8">Something went wrong</p>
<button onClick={reset} className="btn btn-primary">
Reload page
</button>
</div>
</div>
</body>
</html>
);
}Null Safety Patterns
Optional Chaining
// ✅ Safe: Won't crash if any value is null/undefined
const authorName = data.articles?.data[0]?.attributes?.author?.data?.attributes?.name;
// ❌ Unsafe: Will crash if any intermediate value is null
const authorName = data.articles.data[0].attributes.author.data.attributes.name;Nullish Coalescing
// ✅ Provide fallback for null/undefined
const title = article?.title ?? 'Untitled';
const description = article?.description ?? 'No description available';
// ❌ Empty string is falsy but valid
const title = article?.title || 'Untitled'; // Wrong if title is ""Type Guards
function hasImage(article: Article): article is Article & { image: NonNullable<Article['image']> } {
return article.image?.data?.attributes?.url != null;
}
if (hasImage(article)) {
// TypeScript knows article.image exists
return <StrapiImage data={article.image.data.attributes} />;
}Error Logging
Development Logging
lib/strapi.ts
import { createStrapiSDK } from 'strapi-nextgen-framework';
export const strapi = createStrapiSDK({
url: process.env.NEXT_PUBLIC_STRAPI_GRAPHQL_URL!,
logging: {
queries: process.env.NODE_ENV === 'development',
errors: true, // Always log errors
},
});Production Error Tracking
lib/error-tracking.ts
export function logError(error: Error, context?: Record<string, any>) {
if (process.env.NODE_ENV === 'production') {
// Send to error tracking service
// Example: Sentry, Rollbar, etc.
console.error('Error:', error, context);
} else {
console.error('Error:', error, context);
}
}app/page.tsx
import { logError } from '@/lib/error-tracking';
try {
const data = await strapi.getPage('home', GetPageDocument);
} catch (error) {
logError(error as Error, {
page: 'home',
component: 'HomePage',
});
// Show error UI
}Sentry Integration
app/error.tsx
'use client';
import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}Graceful Degradation
Image Loading Errors
import { StrapiImage } from 'strapi-nextgen-framework';
export function ArticleCard({ article }) {
return (
<div className="article-card">
<StrapiImage
data={article.image?.data?.attributes}
nextImageProps={{
alt: article.title,
onError: (e) => {
// Hide broken image
e.currentTarget.style.display = 'none';
},
}}
/>
{/* Fallback if no image */}
{!article.image && (
<div className="placeholder-image bg-gray-200 h-48" />
)}
</div>
);
}Missing Content
export function ArticlePage({ article }) {
return (
<article>
<h1>{article.title || 'Untitled Article'}</h1>
{article.content ? (
<div dangerouslySetInnerHTML={{ __html: article.content }} />
) : (
<p className="text-gray-500 italic">No content available</p>
)}
{article.author?.data?.attributes ? (
<p>By {article.author.data.attributes.name}</p>
) : (
<p>By Unknown Author</p>
)}
</article>
);
}API Errors with Retry
async function fetchWithRetry<T>(
fn: () => Promise<T>,
retries = 3,
delay = 1000
): Promise<T> {
try {
return await fn();
} catch (error) {
if (retries > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithRetry(fn, retries - 1, delay * 2);
}
throw error;
}
}
// Usage
const data = await fetchWithRetry(() =>
strapi.getPage('home', GetPageDocument)
);Testing Error Scenarios
Unit Tests
import { render, screen } from '@testing-library/react';
import { StrapiImage } from 'strapi-nextgen-framework';
test('handles null image data gracefully', () => {
const { container } = render(<StrapiImage data={null} />);
// Should return null, not crash
expect(container.firstChild).toBeNull();
});
test('shows fallback for missing content', () => {
render(<ArticlePage article={{ title: 'Test' }} />);
expect(screen.getByText('No content available')).toBeInTheDocument();
});E2E Tests
import { test, expect } from '@playwright/test';
test('shows 404 for non-existent article', async ({ page }) => {
const response = await page.goto('/blog/non-existent-article');
expect(response?.status()).toBe(404);
await expect(page.locator('h1')).toContainText('404');
});
test('recovers from API error', async ({ page }) => {
// Mock API error
await page.route('**/graphql', route => {
route.abort('failed');
});
await page.goto('/');
// Should show error message, not crash
await expect(page.locator('text=Something went wrong')).toBeVisible();
});Best Practices
1. Fail Gracefully
// ✅ Good: Graceful degradation
if (!article) {
return <NotFoundPage />;
}
// ❌ Bad: Crashes entire page
const title = article.title; // Error if article is null2. Always Provide Fallbacks
// ✅ Good: Always has fallback
<StrapiRenderer
sections={sections}
componentMap={componentMap}
fallback={<DefaultSection />}
/>
// ⚠️ Risky: No fallback
<StrapiRenderer
sections={sections}
componentMap={componentMap}
/>3. Log Errors in Production
// ✅ Good: Track errors
try {
await fetchData();
} catch (error) {
logError(error); // Send to monitoring service
return <ErrorUI />;
}
// ❌ Bad: Silent failures
try {
await fetchData();
} catch (error) {
// Nothing - users and developers don't know
}4. Show User-Friendly Messages
// ✅ Good: User-friendly
<p>We're having trouble loading this content. Please try again.</p>
// ❌ Bad: Technical jargon
<p>GraphQL query failed: Network error at line 42</p>Error Handling Checklist
- Network errors caught and handled
- Error boundaries wrap risky components
- 404 page exists
- Error page exists
- Null checks on all optional data
- Fallback UI for missing content
- Errors logged to monitoring service
- User-friendly error messages
- Error recovery (retry buttons)
- Tested error scenarios
Common Error Scenarios
1. Strapi Offline
Error: Failed to fetch
Handling:
try {
const data = await strapi.getPage('home', GetPageDocument);
} catch (error) {
if (error instanceof TypeError && error.message.includes('fetch')) {
return <OfflineMessage />;
}
throw error;
}2. Invalid GraphQL Query
Error: GraphQL Error: Cannot query field
Handling:
- Check query matches Strapi schema
- Run
npm run codegen - Test query in GraphQL Playground
3. Missing Required Field
Error: TypeError: Cannot read property 'title' of undefined
Handling:
// ✅ Safe
const title = article?.title ?? 'Untitled';
// ❌ Unsafe
const title = article.title;