13  Errors

A fatal error is a failure from which a program cannot safely recover, so execution must stop immediately.

Let’s take a look at this reprex:

library(ambiorix)

app <- Ambiorix$new()

home_get <- function(req, res) {
  xyzabc # intentional fatal error
  res$send("Hello, World!")
}

app$get("/", home_get)

app$start(port = 5000L)

When you run this application and visit http://127.0.0.1:5000/, you get this on the console:

 21-11-2025 22:10:44 Listening on http://127.0.0.1:5000
 21-11-2025 22:10:47 GET on /
object 'xyz' not found

The application ran into a fatal error while executing the handler for “/” because of a missing variable.

Luckily, Ambiorix has a generic global error handler, defined roughly as follows:

error_handler <- function(req, res, error) {
  message(conditionMessage(error))
  res$status <- 500L
  res$send("500: Internal Server Error")
}

Because of that, the application does not crash, and the user gets this on the client:

500: Internal Server Error

13.1 Custom Handler

The generic global error handler for Ambiorix is okay, but you might need a custom one depending on what you’re building.

Just like route handlers, the custom error handler you define must be a function which takes three parameters, in this order:

  1. req: <Required>. The request object.
  2. res: <Required>. The response object.
  3. error: <Required>. The error object. This is the object returned by functions like stop().

For example, if developing a JSON data API, you’d want to return JSON from the handler as follows:

error_handler <- function(req, res, error) {
  message(conditionMessage(error))
  res$status <- 500L

  response <- list(
    status = "error",
    message = "It's not you, it's us :/"
  )

  res$json(response)
}

The HTTP status code of the response given by the error handler should be 5XX eg. 500. Quoting MDN Docs:

The HTTP 500 Internal Server Error server error response status code indicates that the server encountered an unexpected condition that prevented it from fulfilling the request.

This error is a generic “catch-all” response to server issues, indicating that the server cannot find a more appropriate 5XX error to respond with.

After defining a custom error handler, attach it to the app instance by setting the error property:

app$error <- error_handler

Here’s a reprex showing definition and attachment of a custom error handler:

library(ambiorix)

app <- Ambiorix$new(port = 3000L)

error_handler <- function(req, res, error) {
  message(conditionMessage(error))
  res$status <- 503L
  res$send("Oops! We're currently offline. Maintenance underway.")
}

app$error <- error_handler

home_get <- function(req, res) {
  stop("Object 'xyz' not found") # intentional fatal error
  res$send("Hello, World!")
}

app$get("/", home_get)

app$start()

When you visit /, you will see the custom message and not the default one.

As noted in the examples here, we print the real error message to the console, but send back a generic message to the client. This is for security purposes, and it is advised you do the same. It ensures you do not leak critical information about your application to attackers.

13.2 Route Specific

Remember that the last [optional] parameter for HTTP app methods is error?

You can use it to attach an error handler specific to that route. Even when you have a global error handler, the specific handler is the one that will be called for that route.

For example:

home_get <- function(req, res) {
  res$send("Hello, World!")
}

home_get_error_handler <- function(req, res, error) {
  message(conditionMessage(error))
  res$status <- 503L
  res$send("Oops! We're currently offline. Maintenance underway.")
}

Then you attach the handler as follows:

app$get("/", home_get, home_get_error_handler)

Here is a reprex showing the distinction between the global error handler and the route specific one:

library(ambiorix)

app <- Ambiorix$new(port = 3000L)

global_error_hander <- function(req, res, error) {
  message(conditionMessage(error))
  res$status <- 500L
  res$send("Oh no! An error occurred while processing your request :(")
}

app$error <- global_error_hander

home_get <- function(req, res) {
  stop("Object 'xyz' not found") # intentional fatal error
  res$send("Hello, World!")
}

home_get_error_handler <- function(req, res, error) {
  message(conditionMessage(error))
  res$status <- 503L
  res$send("Oops! We're currently offline. Maintenance underway.")
}

about_get <- function(req, res) {
  stop("Object 'xyz' not found") # another intentional fatal error
  res$send("About us.")
}

app$get("/", home_get, home_get_error_handler)
app$get("/about", about_get)

app$start()

Visiting / shows you the route specific error message. On the other hand, /about shows the global error message that we set.