Error pages and logging

When something goes wrong, FloCMS logs the error and shows a page that fits it: a 404 for a missing page, a 403 for a missing permission, a database page when the database is down. In debug mode you get a detailed page for developers instead.

Showing an error page

Throw an HttpException with the status code:

use FloCMS\Core\HttpException;

$post = $this->model()->find($id);

if ($post === null) {
    throw new HttpException(404);
}

FloCMS stops the request, sends the status code and renders the matching page. Auth::authorize() throws a 403 the same way. See roles and permissions.

The error templates

Error pages are files in templates/<template>/errors/. Like other template files, a template only needs the ones it changes; the rest come from templates/default/errors/.

File Used for
404.html HttpException(404): unknown pages, controllers and actions
403.html HttpException(403): missing permissions
500.html Every other error, and every other status code, such as 419 for a missing CSRF token
nodbserver.html The database server can't be reached (status 503)
dberror.html Database setup problems: wrong password, unknown database, not configured (status 500)
queryerror.html A query failed, for example because a table doesn't exist
debug.html The developer page in debug mode
503.html Maintenance mode

Error templates are views, so the template syntax works. They receive:

Variable Contains
$status The HTTP status code
$message A short, safe message, such as "Database not found."
$errorCode The database driver's error code, when there is one
$detail The database driver's own message. Only set in debug mode, because it names hosts and users.

If a template file is missing or fails itself, FloCMS falls back to 500.html, and finally to a plain "Internal Server Error".

Debug mode

With APP_DEBUG=true in .env, errors show the debug page instead: the exception, the code around the line that failed, the stack trace, and the request with its session, GET, POST and server values. Passwords, cookies and Authorization headers are filtered out of the server values.

  • In debug mode, a 404 or 403 also shows the debug page, which helps you see why a URL didn't match.
  • Database setup errors keep their own page in debug mode, with $detail filled in.
  • PHP notices and warnings are turned into exceptions, so an undefined variable or array key stops the page with an error instead of passing silently. This applies in both modes.

Warning

Always set APP_DEBUG=false on a live site. The debug page shows your code, file paths and session data. php flo doctor warns when debug mode is on in production.

Database errors

Models connect to the database on first use, so a page that doesn't query the database works without one. When a query does need it and it isn't there:

  • an unreachable server shows nodbserver.html with status 503
  • a wrong password, an unknown database or empty DB_* settings show dberror.html

To check the database without an error page, for example on a setup screen, use App::dbStatus(), which never throws. See database.

Logging

Every error that reaches the error handler is written to storage/logs/app.log, with the exception class, file and line:

[2026-10-11 14:03:12] error: View file does not exist: /home/user/site/views/blog/show.html {"type":"RuntimeException","file":"...","line":31}

Write your own messages with App::logger(), a PSR-3 logger:

use FloCMS\Core\App;

App::logger()->info('Order placed', ['order' => $orderId]);
App::logger()->warning('Payment provider slow', ['ms' => $duration]);

All PSR-3 levels work: debug, info, notice, warning, error, critical, alert and emergency. The context array is written as JSON. If the log file can't be written, the line goes to PHP's error log instead.

Other logs in storage/logs/:

File Written by
app.log The error handler and App::logger()
api.log The API: errors and router warnings
schedule.log The scheduler

Log commands

php flo log:tail                    # the last 50 lines of the newest log
php flo log:tail api --lines=100    # the last 100 lines of api.log
php flo log:tail app --follow       # keep printing new lines (Ctrl+C to stop)
php flo log:clear app               # empty app.log; without a name, every log (asks first)
php flo log:rotate                  # app.log -> app.log.1 ... for logs over 10 MB

log:rotate takes --max-size= in MB (default 10) and --keep= (default 5 old copies). The scheduler runs it daily, so with the cron job in place you don't need to rotate by hand.

Esc