Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/remix-run/remix/llms.txt

Use this file to discover all available pages before exploring further.

Proper error handling is critical for building resilient applications. Remix provides multiple layers for handling errors at different stages of request processing.

Router-Level Error Handling

Catch errors at the router level to provide consistent error responses:
import { createRouter } from 'remix/fetch-router'

let router = createRouter({
  onError(error, context) {
    console.error('Router error:', error)
    
    if (error instanceof ValidationError) {
      return Response.json(
        { error: error.message, fields: error.fields },
        { status: 400 }
      )
    }

    if (error instanceof NotFoundError) {
      return Response.json(
        { error: 'Resource not found' },
        { status: 404 }
      )
    }

    // Generic error response
    return Response.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  },
})

Middleware Error Handling

Handle errors in middleware:
import type { Middleware } from 'remix/fetch-router'

function errorHandler(): Middleware {
  return async (context, next) => {
    try {
      return await next()
    } catch (error) {
      console.error('Middleware error:', error)

      if (error instanceof SyntaxError) {
        return Response.json(
          { error: 'Invalid JSON in request body' },
          { status: 400 }
        )
      }

      throw error // Re-throw for router-level handler
    }
  }
}

let router = createRouter({
  middleware: [errorHandler()],
})

Action-Level Error Handling

Handle errors in individual actions:
router.post(routes.users, async ({ request }) => {
  try {
    let data = await request.json()
    
    // Validate input
    if (!data.email || !data.name) {
      return Response.json(
        { error: 'Email and name are required' },
        { status: 400 }
      )
    }

    // Create user
    let user = await db.create(users, data)
    
    return Response.json(user, { status: 201 })
  } catch (error) {
    if (error.code === 'UNIQUE_CONSTRAINT') {
      return Response.json(
        { error: 'Email already exists' },
        { status: 409 }
      )
    }
    throw error
  }
})

Custom Error Classes

Create custom error classes for better error handling:
class AppError extends Error {
  constructor(
    message: string,
    public status: number = 500,
    public code?: string
  ) {
    super(message)
    this.name = 'AppError'
  }
}

class ValidationError extends AppError {
  constructor(
    message: string,
    public fields: Record<string, string>
  ) {
    super(message, 400, 'VALIDATION_ERROR')
    this.name = 'ValidationError'
  }
}

class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 404, 'NOT_FOUND')
    this.name = 'NotFoundError'
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401, 'UNAUTHORIZED')
    this.name = 'UnauthorizedError'
  }
}
Usage:
router.get(routes.user, async ({ params }) => {
  let user = await db.find(users, { id: params.id })
  
  if (!user) {
    throw new NotFoundError('User')
  }

  return Response.json(user)
})

Validation Errors

Handle validation errors with detailed field information:
import { parse } from 'remix/data-schema'
import { string, object } from 'remix/data-schema'

let userSchema = object({
  name: string().minLength(2).maxLength(50),
  email: string().email(),
  age: number().min(18).max(120),
})

router.post(routes.users, async ({ request }) => {
  let data = await request.json()
  
  let result = parseSafe(userSchema, data)
  
  if (!result.success) {
    let fieldErrors: Record<string, string> = {}
    
    for (let issue of result.issues) {
      if (issue.path) {
        fieldErrors[issue.path.join('.')] = issue.message
      }
    }

    throw new ValidationError('Validation failed', fieldErrors)
  }

  let user = await db.create(users, result.value)
  return Response.json(user, { status: 201 })
})

Database Errors

Handle database errors gracefully:
router.post(routes.users, async ({ request }) => {
  try {
    let data = await request.json()
    let user = await db.create(users, data)
    return Response.json(user, { status: 201 })
  } catch (error: any) {
    // PostgreSQL unique constraint
    if (error.code === '23505') {
      return Response.json(
        { error: 'Email already exists' },
        { status: 409 }
      )
    }

    // Foreign key violation
    if (error.code === '23503') {
      return Response.json(
        { error: 'Referenced resource does not exist' },
        { status: 400 }
      )
    }

    throw error
  }
})

AbortError Handling

Handle aborted requests:
import { AbortError } from 'remix/fetch-router'

router.get(routes.search, async ({ url }) => {
  let query = url.searchParams.get('q')
  
  try {
    let results = await fetchSearchResults(query)
    return Response.json(results)
  } catch (error) {
    if (error instanceof AbortError) {
      // Request was aborted, don't log as error
      return new Response('Request aborted', { status: 499 })
    }
    throw error
  }
})

Error Responses

Return consistent error response formats:
interface ErrorResponse {
  error: string
  code?: string
  fields?: Record<string, string>
  details?: any
}

function errorResponse(
  message: string,
  status: number,
  options?: {
    code?: string
    fields?: Record<string, string>
    details?: any
  }
): Response {
  let body: ErrorResponse = {
    error: message,
    ...options,
  }

  return Response.json(body, { status })
}

// Usage
return errorResponse('User not found', 404, { code: 'NOT_FOUND' })

Error Logging

Log errors for monitoring and debugging:
function logError(error: Error, context: any) {
  console.error('Error:', {
    message: error.message,
    stack: error.stack,
    url: context.url,
    method: context.method,
    timestamp: new Date().toISOString(),
  })

  // Send to error tracking service (e.g., Sentry, Rollbar)
  // errorTracker.captureException(error, { extra: context })
}

let router = createRouter({
  onError(error, context) {
    logError(error, context)
    return errorResponse('Internal server error', 500)
  },
})

Environment-Specific Errors

Provide different error details based on environment:
let isDevelopment = process.env.NODE_ENV === 'development'

let router = createRouter({
  onError(error, context) {
    logError(error, context)

    if (isDevelopment) {
      // Detailed errors in development
      return Response.json(
        {
          error: error.message,
          stack: error.stack,
          context: {
            url: context.url,
            method: context.method,
          },
        },
        { status: 500 }
      )
    }

    // Generic errors in production
    return errorResponse('Internal server error', 500)
  },
})

Best Practices

  • Always handle errors at multiple levels
  • Use custom error classes for clarity
  • Log errors with context for debugging
  • Return consistent error response formats
  • Don’t expose sensitive information in production
  • Use appropriate HTTP status codes
  • Validate input early
  • Handle database errors specifically

Fetch Router

Router error handling options

Data Schema

Input validation with schemas