Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

The basics

Error handling

When a request ends in an error, Oriel logs it if it should and answers with an error page or JSON. withExceptions() changes both.

Introduction#

A new app handles errors with no setup: an unexpected error that a route or a middleware throws is reported to your log, and the user gets a response with the error's status. To change how errors are reported and rendered, give withExceptions() in bootstrap/app.ts a function that receives the configurator, as the rest of this page shows.

Configuration#

debug in config/app.ts decides how much an error response shows. It reads APP_DEBUG from your .env (see Debug mode), and is false unless the variable says otherwise:

config/app.ts
import { env } from '@orieljs/framework'export default {  name: env('APP_NAME', 'Oriel'),  env: env('APP_ENV', 'production'),  debug: env('APP_DEBUG', false),  url: env('APP_URL', 'http://localhost'),  timezone: 'UTC',  locale: env('APP_LOCALE', 'en'),  cipher: 'AES-256-GCM',  key: env('APP_KEY'),  previous_keys: String(env('APP_PREVIOUS_KEYS', '')).split(',').filter(Boolean),}

A new app's .env sets APP_DEBUG=true, for your machine. With it, a server error shows the debug page: the error's class and message, the request's method and URL, the lines of your code around where it was thrown, the stack, and the errors that caused it. A request that expects JSON gets the message, the error's name, the file, the line and the stack. Errors with an HTTP status, such as a 404, get their usual page either way.

Handling exceptions#

Reporting exceptions#

Reporting an error writes it to your log, at the error level, with the error itself as the entry's exception context. report() registers a function that receives each error to report, to send it somewhere else too, such as an error tracking service:

bootstrap/app.ts
import { dirname } from 'node:path'import { Application } from '@orieljs/framework'import { InvalidOrderException } from '../app/Exceptions/InvalidOrderException'import providers from './providers'export function createApplication(): Promise<Application> {  return Application.configure({ basePath: dirname(import.meta.dirname) })    .withRouting({ web: 'routes/web.ts', console: 'routes/console.ts', health: '/up' })    .withExceptions((exceptions) => {      exceptions.report((error) => {        if (error instanceof InvalidOrderException) {          // ...        }      })    })    .withProviders(providers)    .create()}

The error is still logged afterwards. Return false from the function to stop that, and the functions registered after it:

exceptions.report((error) => {  if (error instanceof InvalidOrderException) {    // ...    return false  }})

Each error is reported once, however many times it reaches the handler, as in Laravel after dontReportDuplicates(). Laravel's global and per-exception log context, context(), isn't in Oriel; Logging shows how to add context to every log entry.

Reporting an error yourself#

Oriel has no report() helper. To report an error and carry on with the request, call the exception handler's report():

async function isValid(value: string): Promise<boolean> {  try {    validateTheValue(value)    return true  } catch (error) {    await app().make(ExceptionHandler).report(error)    return false  }}

Exception log levels#

Every report is logged at the error level. Laravel's level(), which changes the level by the error's class, isn't in Oriel: log the error yourself from a report() function, at the level you want, and return false.

Ignoring exceptions by type#

dontReport() takes the error classes never to report. They are still rendered as usual:

exceptions.dontReport(InvalidOrderException)

Oriel never reports the errors that are part of normal requests: an abort(), a missing route or model, a failed validation, an unauthenticated or unauthorized user, and a CSRF token mismatch. Laravel's dontReportWhen(), stopIgnoring() and ShouldntReport interface aren't in Oriel.

Rendering exceptions#

render() registers a function that turns an error into a response. It receives the error and the request, and returns a Response, or nothing to leave the error to the default rendering:

bootstrap/app.ts
import { dirname } from 'node:path'import { Application, response, View } from '@orieljs/framework'import { InvalidOrderException } from '../app/Exceptions/InvalidOrderException'import providers from './providers'export function createApplication(): Promise<Application> {  return Application.configure({ basePath: dirname(import.meta.dirname) })    .withRouting({ web: 'routes/web.ts', console: 'routes/console.ts', health: '/up' })    .withExceptions((exceptions) => {      exceptions.render(async (error) => {        if (error instanceof InvalidOrderException) {          return response(await View.make('errors.invalid-order').render(), 500)        }      })    })    .withProviders(providers)    .create()}

The functions run first, for every error, so they can change how a built-in error renders too:

exceptions.render((error, request) => {  if (error instanceof ModelNotFoundError && (request as HttpRequest).is('api/*')) {    return response().json({ message: 'Record not found.' }, 404)  }})

The request is typed unknown: during a request it is the request request() returns.

Rendering exceptions as JSON#

An error is rendered as JSON when the request expects JSON: its Accept header asks for JSON first, or it is an AJAX request that accepts anything (see content negotiation). Otherwise it gets an HTML page. With shouldRenderJsonWhen(), you decide it yourself; the API starter kit answers JSON for every route under /api:

exceptions.shouldRenderJsonWhen((request) => request.is('api/*') || request.expectsJson())

Laravel's respond(), which changes every response the handler renders, isn't in Oriel: use render().

Renderable exceptions#

An error class of your own can render itself. Give it a render() method that returns a Response:

app/Exceptions/InvalidOrderException.ts
import { response } from '@orieljs/framework'export class InvalidOrderException extends Error {  /** Render the exception as an HTTP response. */  render(request: unknown): Response {    return response('The order could not be processed.', 422)  }}

When render() returns something that isn't a Response, the error is rendered as usual. A toResponse() method works the same way. The error is still reported, unless its class is in dontReport(). Laravel's reportable exceptions, with a report() method of their own, aren't in Oriel: use a report() function in withExceptions().

Laravel's throttle(), which samples or rate limits the reports, isn't in Oriel either.

HTTP exceptions#

abort() ends the request with an HTTP status, from anywhere in your code:

abort(404)

A message and headers can follow. The message is what JSON responses show; the HTML pages show it for a 403 only:

abort(403, 'You may not edit this post.')abort(503, 'Down for maintenance', { 'Retry-After': '120' })

abortIf() and abortUnless() abort when a condition is true, or false:

abortIf(user === undefined, 403)abortUnless(post.published_at !== null, 404)

abort() also takes a ready Response, which is sent as it is: abort(response('Gone fishing', 418)).

Statuses by error#

Oriel's own errors carry their statuses. Each gets the same page, or JSON, as an abort() with that status:

ErrorStatus
No route matches the URL404
The route doesn't take the method405, with an Allow header
A model not found, by findOrFail() or route model binding404
A guest on a route with the auth middleware401 as JSON; otherwise a redirect to the login page
An authorization check that fails403, or the status the policy chose
A form without its CSRF token419
A failed validation422 as JSON; otherwise a redirect back with the errors
Too many requests on a throttle route429, with Retry-After and the X-RateLimit-* headers
A body over the size limit413
A JSON body nested too deeply400
Anything else500

None of these is reported, except the last. The login page a guest is sent to is http.redirect_guests_to, set with redirectGuestsTo(), else the route named login, else /login; the URL the guest wanted is kept for after they sign in.

Custom HTTP error pages#

To show your own page for a status, add a view named after it: resources/views/errors/404.tsx for a 404. The view receives the status, the message and the exception:

resources/views/errors/404.tsx
interface Props {  status: number  message: string  exception: unknown}export default function NotFound({ status, message, exception }: Props) {  return (    <html lang="en">      <body>        <h1>{status}</h1>        <h2>{exception instanceof Error && exception.message !== '' ? exception.message : message}</h2>      </body>    </html>  )}

message is the status's text, "Not Found", except for a 403, where it is the error's message. The error's own message is on exception, and is empty for an abort(404) without one. A new app has no error views. Without one, the page is a minimal one with the status and its text, as 404 | Not Found.

The pages are views like any other, so they can use your layout. A view that fails to render is reported, and the minimal page is sent in its place.

Fallback HTTP error pages#

resources/views/errors/4xx.tsx is the page for any client error without a page of its own, and 5xx.tsx for any server error.

What a production error shows#

With debug off, a server error shows nothing of the error: your errors/500.tsx or errors/5xx.tsx view, or the minimal "500 | Server Error", and { "message": "Server Error" } to a request that expects JSON. The error is reported to your log with its stack.

An error with an HTTP status shows its status text on the HTML page (a 403 shows its message), and its message in JSON: { "message": "No query results for model [Post] 5" } for a missing Post. Keep secrets out of the messages you give abort().

Privacy policy ·

esc