Runtime Errors
Troubleshooting runtime errors, component issues, and application crashes during development and production.
Runtime Errors
This section addresses errors that occur while the application is running, including component rendering issues, API route problems, and application crashes. These errors can affect both development and production environments.
Component Rendering Errors
Issue: Hydration mismatch errors
Symptoms:
- Console warnings about hydration
- Different server/client rendering
- Content "jumping" or changing after page load
- React hydration warnings
Understanding Hydration: Hydration occurs when React takes over the server-rendered HTML and makes it interactive. Mismatches happen when the server-rendered content differs from what React expects on the client.
Common Causes and Solutions:
// ❌ Problem: Using browser-only APIs during SSR
function Component() {
const [mounted, setMounted] = useState(false)
// This will cause hydration mismatch
const theme = localStorage.getItem('theme') || 'light'
return <div className={theme}>Content</div>
}
// ✅ Solution: Use useEffect to handle client-only code
function Component() {
const [mounted, setMounted] = useState(false)
const [theme, setTheme] = useState('light')
useEffect(() => {
setMounted(true)
setTheme(localStorage.getItem('theme') || 'light')
}, [])
if (!mounted) {
return <div className="light">Content</div> // Consistent with SSR
}
return <div className={theme}>Content</div>
}
// ✅ Alternative: Use dynamic imports with ssr: false
const ClientOnlyComponent = dynamic(() => import('./ClientOnlyComponent'), {
ssr: false,
loading: () => <div>Loading...</div>
})
function App() {
return (
<div>
<h1>Server-rendered content</h1>
<ClientOnlyComponent />
</div>
)
}
Date/Time Hydration Issues:
// ❌ Problem: Different date formatting on server/client
function TimeDisplay() {
const now = new Date().toLocaleString()
return <span>{now}</span>
}
// ✅ Solution: Consistent date handling
function TimeDisplay() {
const [time, setTime] = useState<string>()
useEffect(() => {
setTime(new Date().toLocaleString())
}, [])
// Show placeholder during hydration
return <span>{time || '...'}</span>
}
// ✅ Better solution: Use suppressHydrationWarning for time-sensitive content
function TimeDisplay() {
const [time, setTime] = useState(() => new Date().toLocaleString())
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date().toLocaleString())
}, 1000)
return () => clearInterval(timer)
}, [])
return <span suppressHydrationWarning>{time}</span>
}
Random Data Hydration Issues:
// ❌ Problem: Random data changes between server and client
function RandomQuote() {
const quotes = ['Quote 1', 'Quote 2', 'Quote 3']
const randomQuote = quotes[Math.floor(Math.random() * quotes.length)]
return <blockquote>{randomQuote}</blockquote>
}
// ✅ Solution: Deterministic initial state
function RandomQuote() {
const quotes = ['Quote 1', 'Quote 2', 'Quote 3']
const [quote, setQuote] = useState(quotes[0]) // Consistent initial state
useEffect(() => {
// Randomize after hydration
setQuote(quotes[Math.floor(Math.random() * quotes.length)])
}, [])
return <blockquote>{quote}</blockquote>
}
Issue: "Cannot read properties of undefined" errors
Symptoms:
- Runtime errors accessing undefined properties
- Application crashes with property access errors
- Intermittent errors when data is loading
Solutions:
// ❌ Problem: Accessing nested properties without checks
function UserProfile({ user }) {
return (
<div>
<h1>{user.profile.name}</h1>
<p>{user.profile.email}</p>
<img src={user.profile.avatar.url} alt="Avatar" />
</div>
)
}
// ✅ Solution 1: Optional chaining and nullish coalescing
function UserProfile({ user }) {
return (
<div>
<h1>{user?.profile?.name ?? 'Unknown User'}</h1>
<p>{user?.profile?.email ?? 'No email'}</p>
<img
src={user?.profile?.avatar?.url ?? '/default-avatar.png'}
alt="Avatar"
/>
</div>
)
}
// ✅ Solution 2: Default values with destructuring
function UserProfile({ user = {} }) {
const {
profile: {
name = 'Unknown User',
email = 'No email',
avatar: { url: avatarUrl = '/default-avatar.png' } = {}
} = {}
} = user
return (
<div>
<h1>{name}</h1>
<p>{email}</p>
<img src={avatarUrl} alt="Avatar" />
</div>
)
}
// ✅ Solution 3: Type guards with early returns
function UserProfile({ user }) {
if (!user?.profile) {
return <div>Loading user profile...</div>
}
const { name, email, avatar } = user.profile
return (
<div>
<h1>{name || 'Unknown User'}</h1>
<p>{email || 'No email'}</p>
<img
src={avatar?.url || '/default-avatar.png'}
alt="Avatar"
/>
</div>
)
}
// ✅ Solution 4: Using custom hook for safe data access
function useSafeUser(user) {
return useMemo(() => {
if (!user) return null
return {
name: user.profile?.name || 'Unknown User',
email: user.profile?.email || 'No email',
avatarUrl: user.profile?.avatar?.url || '/default-avatar.png',
isComplete: Boolean(user.profile?.name && user.profile?.email)
}
}, [user])
}
function UserProfile({ user }) {
const safeUser = useSafeUser(user)
if (!safeUser) {
return <div>Loading...</div>
}
return (
<div>
<h1>{safeUser.name}</h1>
<p>{safeUser.email}</p>
<img src={safeUser.avatarUrl} alt="Avatar" />
{!safeUser.isComplete && <p>Profile incomplete</p>}
</div>
)
}
Issue: State update errors
Symptoms:
- "Cannot update component while rendering" warnings
- Infinite re-render loops
- State not updating as expected
Solutions:
// ❌ Problem: Setting state during render
function Counter() {
const [count, setCount] = useState(0)
// This causes infinite loop
if (count > 10) {
setCount(0) // ❌ Don't do this
}
return <div>{count}</div>
}
// ✅ Solution: Use useEffect for side effects
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
if (count > 10) {
setCount(0)
}
}, [count])
return <div>{count}</div>
}
// ✅ Better solution: Handle logic in event handlers
function Counter() {
const [count, setCount] = useState(0)
const increment = () => {
setCount(prev => prev + 1 > 10 ? 0 : prev + 1)
}
return (
<div>
<div>{count}</div>
<button onClick={increment}>Increment</button>
</div>
)
}
// ❌ Problem: Stale closure in useEffect
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1) // ❌ Stale closure
}, 1000)
return () => clearInterval(timer)
}, []) // Empty dependency array causes stale closure
return <div>{count}</div>
}
// ✅ Solution: Use functional state updates
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1) // ✅ Always gets current value
}, 1000)
return () => clearInterval(timer)
}, [])
return <div>{count}</div>
}
API Route Errors
Issue: API routes returning 404 errors
Symptoms:
- API endpoints not found
- Routing not working correctly
- Method not allowed errors
File Structure Check:
# ✅ Correct App Router structure
app/
├── api/
│ ├── products/
│ │ ├── route.ts # /api/products
│ │ └── [id]/
│ │ └── route.ts # /api/products/[id]
│ ├── auth/
│ │ └── callback/
│ │ └── route.ts # /api/auth/callback
│ └── webhooks/
│ └── stripe/
│ └── route.ts # /api/webhooks/stripe
# ❌ Incorrect structure (Pages Router style)
pages/
├── api/
│ ├── products.ts # Don't use in App Router
│ └── auth/
│ └── callback.ts # Don't use in App Router
Route Handler Implementation:
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
// ✅ Correct: Named exports for HTTP methods
export async function GET(request: NextRequest) {
try {
const supabase = createClient()
const { data, error } = await supabase
.from('products')
.select('*')
if (error) throw error
return NextResponse.json({ data })
} catch (error) {
console.error('API Error:', error)
return NextResponse.json(
{ error: 'Failed to fetch products' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const supabase = createClient()
const { data, error } = await supabase
.from('products')
.insert(body)
.select()
if (error) throw error
return NextResponse.json({ data }, { status: 201 })
} catch (error) {
console.error('API Error:', error)
return NextResponse.json(
{ error: 'Failed to create product' },
{ status: 500 }
)
}
}
// ❌ Wrong: Default export (Pages Router style)
// export default function handler(req, res) { ... }
Dynamic Route Parameters:
// app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const productId = params.id
// Validate parameter
if (!productId || typeof productId !== 'string') {
return NextResponse.json(
{ error: 'Invalid product ID' },
{ status: 400 }
)
}
const supabase = createClient()
const { data, error } = await supabase
.from('products')
.select('*')
.eq('id', productId)
.single()
if (error) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
)
}
return NextResponse.json({ data })
} catch (error) {
console.error('API Error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
Issue: CORS errors
Symptoms:
- Cross-origin request blocked
- Preflight request failures
- Browser security errors
Solutions:
// Method 1: Add CORS headers to specific routes
export async function GET(request: NextRequest) {
const data = await fetchData()
return new NextResponse(JSON.stringify(data), {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Content-Type': 'application/json',
},
})
}
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
// Method 2: Use middleware for global CORS
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
// Handle CORS
if (request.nextUrl.pathname.startsWith('/api/')) {
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set(
'Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE, OPTIONS'
)
response.headers.set(
'Access-Control-Allow-Headers',
'Content-Type, Authorization'
)
return response
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*'
}
// Method 3: CORS utility function
function corsResponse(data: any, status: number = 200) {
return new NextResponse(JSON.stringify(data), {
status,
headers: {
'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN || '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Content-Type': 'application/json',
},
})
}
// Usage
export async function GET(request: NextRequest) {
try {
const data = await fetchData()
return corsResponse({ data })
} catch (error) {
return corsResponse({ error: 'Failed to fetch data' }, 500)
}
}
Issue: Request/Response handling errors
Symptoms:
- JSON parsing errors
- Request body not accessible
- Response formatting issues
Solutions:
// ✅ Proper request handling
export async function POST(request: NextRequest) {
try {
// Check content type
const contentType = request.headers.get('content-type')
if (!contentType?.includes('application/json')) {
return NextResponse.json(
{ error: 'Content-Type must be application/json' },
{ status: 400 }
)
}
// Parse JSON with error handling
let body
try {
body = await request.json()
} catch (error) {
return NextResponse.json(
{ error: 'Invalid JSON in request body' },
{ status: 400 }
)
}
// Validate required fields
const { name, email } = body
if (!name || !email) {
return NextResponse.json(
{ error: 'Name and email are required' },
{ status: 400 }
)
}
// Process request
const result = await processData(body)
return NextResponse.json(
{
message: 'Success',
data: result,
timestamp: new Date().toISOString()
},
{ status: 201 }
)
} catch (error) {
console.error('API Error:', error)
// Don't expose internal errors
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
// ✅ Handle different content types
export async function POST(request: NextRequest) {
const contentType = request.headers.get('content-type')
let body
if (contentType?.includes('application/json')) {
body = await request.json()
} else if (contentType?.includes('multipart/form-data')) {
body = await request.formData()
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
const formData = await request.formData()
body = Object.fromEntries(formData)
} else {
body = await request.text()
}
// Process based on content type
return NextResponse.json({ received: body })
}
// ✅ Query parameter handling
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
// Get query parameters
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const search = searchParams.get('search') || ''
// Validate parameters
if (page < 1 || limit < 1 || limit > 100) {
return NextResponse.json(
{ error: 'Invalid pagination parameters' },
{ status: 400 }
)
}
const data = await fetchPaginatedData({ page, limit, search })
return NextResponse.json({
data: data.items,
pagination: {
page,
limit,
total: data.total,
totalPages: Math.ceil(data.total / limit)
}
})
}
Error Boundary Implementation
Custom Error Boundaries
// components/ErrorBoundary.tsx
'use client'
import React, { Component, ErrorInfo, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
onError?: (error: Error, errorInfo: ErrorInfo) => void
}
interface State {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false
}
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo)
// Call custom error handler
this.props.onError?.(error, errorInfo)
// Send to error reporting service
this.reportError(error, errorInfo)
}
private reportError = (error: Error, errorInfo: ErrorInfo) => {
// Send to Sentry, LogRocket, or other error reporting service
if (typeof window !== 'undefined') {
// Client-side error reporting
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: {
message: error.message,
stack: error.stack,
name: error.name
},
errorInfo,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
})
}).catch(console.error)
}
}
public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback
}
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.stack}</pre>
</details>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
)
}
return this.props.children
}
}
// Usage
function App() {
return (
<ErrorBoundary
fallback={<div>Application error occurred</div>}
onError={(error, errorInfo) => {
console.log('Custom error handler:', error)
}}
>
<MyComponent />
</ErrorBoundary>
)
}
Async Error Handling
// Hook for async error handling
function useAsyncError() {
const [, setError] = useState()
return useCallback((error: Error) => {
setError(() => {
throw error
})
}, [])
}
// Component with async error handling
function AsyncComponent() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const throwError = useAsyncError()
const fetchData = async () => {
try {
setLoading(true)
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result = await response.json()
setData(result)
} catch (error) {
// This will be caught by ErrorBoundary
throwError(error as Error)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchData()
}, [])
if (loading) return <div>Loading...</div>
return <div>{JSON.stringify(data)}</div>
}
Memory Leak Prevention
Cleanup Patterns
// ✅ Proper cleanup patterns
function ComponentWithCleanup() {
const [data, setData] = useState([])
useEffect(() => {
let mounted = true
// Event listeners
const handleResize = () => {
if (mounted) {
// Handle resize
}
}
window.addEventListener('resize', handleResize)
// Subscriptions
const subscription = supabase
.channel('products')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'products' },
(payload) => {
if (mounted) {
setData(prev => [...prev, payload.new])
}
}
)
.subscribe()
// Timers
const timer = setInterval(() => {
if (mounted) {
// Do something
}
}, 1000)
// Cleanup function
return () => {
mounted = false
window.removeEventListener('resize', handleResize)
subscription.unsubscribe()
clearInterval(timer)
}
}, [])
return <div>{/* Component content */}</div>
}
// ✅ AbortController for fetch requests
function ComponentWithFetch() {
const [data, setData] = useState(null)
useEffect(() => {
const controller = new AbortController()
async function fetchData() {
try {
const response = await fetch('/api/data', {
signal: controller.signal
})
if (!response.ok) throw new Error('Fetch failed')
const result = await response.json()
setData(result)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error)
}
}
}
fetchData()
return () => {
controller.abort()
}
}, [])
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>
}
This comprehensive runtime errors guide provides solutions for the most common issues encountered during application execution, with practical examples and preventive measures.