agentsclimarketplace

Backend error handling

Skill kensaurus/cursor-kenji/skills/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

Install
npx -y skills add kensaurus/cursor-kenji --skill backend-error-handling

Assembled 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:

  1. Check for existing error types:
rg "type.*Error|interface.*Error" --type ts
rg "ActionResult|ApiError" --type ts
  1. Check for existing error boundaries:
ls -la app/error.tsx app/global-error.tsx
rg "ErrorBoundary" --type tsx
  1. Check for existing error utilities:
rg "formatError|handleError|reportError" --type ts
ls -la src/lib/errors* src/lib/error* 2>/dev/null # @/lib/errors
  1. 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 Actions
  • useFormStatus - Pending state in child components
  • useOptimistic - 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()}
 />
 )
 }

 // ...
}

Further reading

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.