Backend error handling
π¦Curated Cursor AI agent skills, slash commands, MCP configs, subagents & rules for full-stack dev β React 19, Next.js 15, Supabase, Tailwind v4, TypeScript
npx -y skills add kensaurus/cursor-kenji --skill backend-error-handlingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Implement solid error handling patterns. Use when adding error handling, improving error UX, debugging error flows, standardizing error responses, or when user mentions "error boundary", "try/catch", "error state", "toast notification", "form validation error", or "API error handling".
The file declares its own license as MIT. That is the authorβs claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
10.1 KB, as published. Nobody here has run it
Error Handling Skill
full error handling patterns for full-stack applications.
When to Use
- Adding error handling to new features
- Improving error user experience
- Standardizing error responses
- Debugging error propagation
- Adding error monitoring
CRITICAL: Check Existing First
Before adding ANY error handling, verify:
- Check for existing error types:
rg "type.*Error|interface.*Error" --type ts
rg "ActionResult|ApiError" --type ts
- Check for existing error boundaries:
ls -la app/error.tsx app/global-error.tsx
rg "ErrorBoundary" --type tsx
- Check for existing error utilities:
rg "formatError|handleError|reportError" --type ts
ls -la src/lib/errors* src/lib/error* 2>/dev/null # @/lib/errors
- Check established error response patterns:
rg "success: false|error:" src/features/*/server/ --type ts | head -10
Why: Inconsistent error handling confuses users and complicates debugging. Always follow established patterns.
Error Handling Layers
βββββββββββββββββββββββββββββββββββββββββββ
β UI Layer β
β - Error boundaries β
β - Form validation errors β
β - Toast notifications β
βββββββββββββββββββββββββββββββββββββββββββ€
β Application Layer β
β - Server Action errors β
β - API route errors β
β - Business logic errors β
βββββββββββββββββββββββββββββββββββββββββββ€
β Data Layer β
β - Database errors β
β - Validation errors (Zod) β
β - External API errors β
βββββββββββββββββββββββββββββββββββββββββββ
Standard Error Types
// types/errors.ts
// Base error shape
interface AppError {
code: string // Machine-readable: VALIDATION_ERROR
message: string // User-friendly message
details?: unknown // Additional context
}
// Action result pattern
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: AppError }
// Common error codes
const ErrorCode = {
VALIDATION_ERROR: 'VALIDATION_ERROR',
NOT_FOUND: 'NOT_FOUND',
UNAUTHORIZED: 'UNAUTHORIZED',
FORBIDDEN: 'FORBIDDEN',
CONFLICT: 'CONFLICT',
RATE_LIMITED: 'RATE_LIMITED',
INTERNAL_ERROR: 'INTERNAL_ERROR',
} as const
Server Action Error Handling
// features/users/server/actions.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
const CreateUserSchema = z.object({
email: z.string().email('Invalid email address'),
name: z.string().min(1, 'Name is required'),
})
export async function createUser(
prevState: ActionResult<User>,
formData: FormData
): Promise<ActionResult<User>> {
try {
// 1. Validate input
const validated = CreateUserSchema.safeParse({
email: formData.get('email'),
name: formData.get('name'),
})
if (!validated.success) {
return {
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: validated.error.flatten().fieldErrors,
},
}
}
// 2. Check authorization
const session = await auth()
if (!session) {
return {
success: false,
error: {
code: 'UNAUTHORIZED',
message: 'Please sign in to continue',
},
}
}
// 3. Execute business logic
const user = await db.user.create({
data: validated.data,
})
revalidatePath('/users')
return { success: true, data: user }
} catch (error) {
// 4. Handle known errors
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
return {
success: false,
error: {
code: 'CONFLICT',
message: 'A user with this email already exists',
},
}
}
}
// 5. Log unknown errors, return generic message
console.error('createUser error:', error)
return {
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'Something went wrong. Please try again.',
},
}
}
}
Form Error Display (React 19+)
// components/UserForm.tsx
'use client'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import { createUser } from '@/features/users/server/actions'
// Separate submit button to use useFormStatus
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create User'}
</button>
)
}
export function UserForm() {
const [state, action, isPending] = useActionState(createUser, null)
// Get field errors from validation
const fieldErrors = state?.success === false
? state.error.details as Record<string, string[]>
: {}
return (
<form action={action}>
{/* Global error */}
{state?.success === false && state.error.code !== 'VALIDATION_ERROR' && (
<div role="alert" className="bg-red-50 text-red-700 p-3 rounded-lg mb-4">
{state.error.message}
</div>
)}
{/* Field with error */}
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
aria-invalid={!!fieldErrors.email}
aria-describedby={fieldErrors.email ? 'email-error' : undefined}
className={fieldErrors.email ? 'border-red-500' : ''}
/>
{fieldErrors.email && (
<p id="email-error" className="text-red-600 text-sm mt-1">
{fieldErrors.email[0]}
</p>
)}
</div>
<SubmitButton />
</form>
)
}
React 19 Form Patterns:
useActionState- Form state with Server ActionsuseFormStatus- Pending state in child componentsuseOptimistic- Optimistic UI updates
## React Error Boundaries
```tsx
// app/error.tsx (Next.js page error boundary)
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log to error reporting service
console.error('Page error:', error)
}, [error])
return (
<div className="flex flex-col items-center justify-center min-h-[400px]">
<h2 className="text-xl font-semibold mb-4">Something went wrong</h2>
<p className="text-muted-foreground mb-6">
We're sorry, but something unexpected happened.
</p>
<button
onClick={reset}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg"
>
Try again
</button>
</div>
)
}
// app/global-error.tsx (root error boundary)
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</body>
</html>
)
}
API Route Error Handling
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = ProductSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid request body',
details: validated.error.flatten().fieldErrors,
},
},
{ status: 400 }
)
}
const product = await db.product.create({ data: validated.data })
return NextResponse.json({ data: product }, { status: 201 })
} catch (error) {
if (error instanceof SyntaxError) {
return NextResponse.json(
{ error: { code: 'INVALID_JSON', message: 'Invalid JSON body' } },
{ status: 400 }
)
}
console.error('POST /api/products error:', error)
return NextResponse.json(
{ error: { code: 'INTERNAL_ERROR', message: 'Internal server error' } },
{ status: 500 }
)
}
}
TanStack Query Error Handling
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
export function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: async () => {
const res = await fetch('/api/products')
if (!res.ok) {
const error = await res.json()
throw new Error(error.error?.message || 'Failed to fetch products')
}
return res.json()
},
retry: (failureCount, error) => {
// Don't retry on 4xx errors
if (error.message.includes('401') || error.message.includes('403')) {
return false
}
return failureCount < 3
},
})
}
export function useCreateProduct() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (data: ProductInput) => {
const res = await fetch('/api/products', {
method: 'POST',
body: JSON.stringify(data),
})
if (!res.ok) {
const error = await res.json()
throw new Error(error.error?.message || 'Failed to create product')
}
return res.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] })
toast.success('Product created')
},
onError: (error) => {
toast.error(error.message)
},
})
}
Error State UI Components
// components/ErrorState.tsx
import { AlertCircle, RefreshCw } from 'lucide-react'
interface ErrorStateProps {
title?: string
message: string
onRetry?: () => void
}
export function ErrorState({
title = 'Error',
message,
onRetry
}: ErrorStateProps) {
return (
<div className="flex flex-col items-center justify-center py-12 text-center">
<AlertCircle className="h-12 w-12 text-red-500 mb-4" />
<h3 className="font-semibold text-lg mb-2">{title}</h3>
<p className="text-muted-foreground mb-6 max-w-sm">{message}</p>
{onRetry && (
<button
onClick={onRetry}
className="inline-flex items-center gap-2 px-4 py-2 border rounded-lg hover:bg-muted"
>
<RefreshCw className="h-4 w-4" />
Try again
</button>
)}
</div>
)
}
// Usage with TanStack Query
function ProductList() {
const { data, error, isLoading, refetch } = useProducts()
if (error) {
return (
<ErrorState
title="Failed to load products"
message={error.message}
onRetry={() => refetch()}
/>
)
}
// ...
}