Skip to content

Error handling

The Boring JavaScript Stack handles errors in two different modes:

  • development errors are for developers and should show useful debugging context
  • production errors are for users and should render calm, branded error pages

For Inertia requests, this matters because the failing request usually happens over XHR. In development, Inertia shows non-Inertia error responses in its error modal so you can see the real failure without digging through the Network tab.

Development errors

In development (NODE_ENV !== 'production'), unexpected server errors are rendered with Youch.

Youch gives Boring Stack apps a rich stack trace with source snippets, request context, and readable frames. When the failure happens during an Inertia visit or form submission, Inertia displays that Youch HTML inside its development error modal. When the same failure happens on a normal browser visit, the browser shows the Youch page directly.

The development error response includes useful debugging context:

  • HTTP status code
  • error name and message
  • request method and URL
  • stack frames with source snippets
  • sanitized headers, params, query, body, and session data

Sensitive values such as cookies, authorization headers, CSRF tokens, passwords, secrets, and session-looking values are redacted before they are shown. Even in development, this keeps screenshots, recordings, and copied error reports safer.

Inertia's modal flash

Inertia opens its development error modal and then writes the error HTML into an iframe. On slower paints, you may briefly see the modal shell before Youch finishes rendering. This is an upstream modal timing detail, not a Boring Stack error-handling requirement. We do not ship an extra client patch for it.

Content Security Policy

If your app uses a strict Content Security Policy that blocks inline styles, pass a nonce to createInertiaApp() so Inertia can attach it to the style tag used by the development error modal. This helps the modal styles load under CSP, but it does not change server error handling.

Production errors

In production, users should not see stack traces or internal exception messages.

inertia-sails renders a configured Inertia error page for the common production statuses:

  • 403
  • 404
  • 500
  • 503

The Boring Stack templates ship an error page under assets/js/pages/error.*. That page receives:

js
{
  status: 404,
  title: 'Page not found',
  message: 'The page you are looking for could not be found.'
}

This keeps production errors inside your app's visual language while still letting Sails handle the HTTP status code correctly.

Using the serverError response

Define the serverError exit in your action:

js
module.exports = {
  exits: {
    success: {
      responseType: 'inertia'
    },
    serverError: {
      responseType: 'serverError'
    }
  },

  fn: async function () {
    try {
      const data = await someRiskyOperation()
      return { page: 'dashboard', props: { data } }
    } catch (error) {
      throw { serverError: error }
    }
  }
}

Direct usage

You can also call handleServerError directly:

js
module.exports = {
  fn: async function () {
    try {
      await dangerousOperation()
    } catch (error) {
      return sails.inertia.handleServerError(this.req, this.res, error)
    }
  }
}

Behavior by environment

EnvironmentInertia requestNon-Inertia request
DevelopmentYouch inside the Inertia development modalFull Youch HTML page
ProductionInertia error page for configured statusInertia error page when configured, otherwise EJS/JSON

Hybrid apps and Sails error pages

Boring Stack apps can be hybrid: some routes return Inertia pages and other routes still render traditional Sails/EJS views. Keep the normal Sails 404.ejs and 500.ejs pages for non-Inertia requests. They are still the right fallback for direct browser visits, server-rendered EJS routes, crawlers, and any page that is not controlled by the Inertia client.

Only add Inertia-aware notFound or forbidden responses when you specifically want Sails responses to render through the Inertia status-page policy. If errorPage is configured, full-page browser requests can render the Inertia error component too. If your app wants EJS for non-Inertia requests and Inertia error pages only for X-Inertia requests, branch in your custom response:

js
// api/responses/notFound.js
module.exports = function notFound(error) {
  if (this.req.header('X-Inertia')) {
    return this.req._sails.inertia.handleErrorPage(this.req, this.res, {
      statusCode: 404,
      error
    })
  }

  return this.res.status(404).view('404', { error })
}

A good rule is:

  • non-Inertia request: use the Sails/EJS response
  • Inertia request: either redirect/flash, show the development error modal, or render an Inertia error page if the app has one

Do not replace Sails' built-in error handling globally unless every route in the app is truly Inertia-only.

Custom responses

The Boring Stack templates include notFound and forbidden responses that route production-friendly statuses through the Inertia error page:

js
// api/responses/notFound.js
module.exports = function notFound(error) {
  return this.req._sails.inertia.handleErrorPage(this.req, this.res, {
    statusCode: 404,
    error
  })
}

Use the same shape for other status pages that should render through Inertia.

Customizing development errors

The development error HTML is generated by Youch. If you need to customize it, you can create your own response handler:

js
// api/responses/serverError.js
module.exports = function serverError(error) {
  const sails = this.req._sails
  const isDev = process.env.NODE_ENV !== 'production'
  const isInertia = this.req.header('X-Inertia')

  if (isInertia && isDev) {
    return this.res.status(500).send(buildCustomErrorHtml(error))
  }

  // Fall back to default handling
  return sails.inertia.handleServerError(this.req, this.res, error)
}

Global error handling

For catching unhandled errors globally, you can use Sails' config/http.js middleware:

js
// config/http.js
module.exports.http = {
  middleware: {
    order: [
      // ... other middleware
      'errorHandler'
    ],

    errorHandler: function (err, req, res, next) {
      if (req.header('X-Inertia')) {
        return sails.inertia.handleServerError(req, res, err)
      }
      return next(err)
    }
  }
}

Error handling best practices

  1. Use specific exits: Define clear exit types for different error scenarios
js
exits: {
  success: { responseType: 'inertia' },
  notFound: { responseType: 'notFound' },
  badRequest: { responseType: 'badRequest' },
  serverError: { responseType: 'serverError' }
}
  1. Log errors: Always log server errors for monitoring
js
catch (error) {
  sails.log.error('Payment processing failed:', error)
  throw { serverError: error }
}
  1. User-friendly production errors: Use the shared error page for status pages and flash messages for recoverable failures
js
catch (error) {
  sails.log.error('Operation failed:', error)
  sails.inertia.flash('error', 'Something went wrong. Please try again.')
  return '/dashboard'
}

Validation errors vs server errors

TypeResponseUse case
badRequest400 + redirect with errorsForm validation failures
serverError500 + error modal/redirectUnexpected exceptions

Use badRequest for validation errors (user can fix) and serverError for unexpected failures (bugs, external service failures).

All open source projects are released under the MIT License.