Error Handling
AppError- Default error pages
- Panics don’t take the server down
APP_DEBUG- Logging
xr dev’s build-status banner- Next
AppError
Every handler in this framework returns Result<impl IntoResponse,
AppError> (see Controllers & Requests
for the full enum). ? propagates naturally from any sqlx/AppError-
returning call, the same as any other Rust Result-returning function:
pub async fn show(post: Post) -> Result<impl IntoResponse, AppError> {
let author = post.user().await?; // AppError::Internal on a DB failure
larust_support::auth::authorize(post.can_manage(&user).await?)?; // 403 if false
Ok(view!("posts.show", { post }))
}
Default error pages
A generated app registers its error pages once, at boot:
app.with_error_pages(larust_core::ErrorPages {
not_found: larust_support::error_view!("404"),
internal: larust_support::error_view!("500"),
})
error_view!("404") looks for resources/views/errors/404.blade.xr in
your app and compiles it the same way view! compiles any other
template; drop a file there to override either page. With no file
present, it compiles to Larust’s own built-in default page instead - a
plain, real 404/500 you never have to build from scratch just to have
something reasonable in production.
Panics don’t take the server down
Every request runs behind tower_http’s CatchPanicLayer - if a handler
panics (an unwrapped None, an index out of bounds, whatever), that one
request gets a 500 response and the rest of the server keeps running
completely unaffected. This is standard tower-http behavior, not
something Larust built itself, but it’s on unconditionally in every
generated app.
APP_DEBUG
# .env
APP_DEBUG=true
With APP_DEBUG=true, an AppError::Internal/Config (or a caught
panic) renders a real, descriptive HTML page instead of a generic
message, in a Laravel/Ignition-style layout: the top-level failure and
every source() cause each get their own card, with a light/dark/system
theme toggle in the corner. This is the single most useful thing to have
on while actually building a feature - a broken query tells you why it
broke, right there in the browser, instead of a bare “internal server
error.”
For a caught panic specifically, the page also renders the real Rust
backtrace captured at the moment it happened, with known stdlib/runtime/
tokio-internal frames collapsed into a “N runtime frames hidden”
<details> region so your own app frames aren’t buried in noise -
expand any of those regions if you do need to see what’s underneath.
Getting a backtrace at all still depends on the standard
RUST_BACKTRACE
env var being set (RUST_BACKTRACE=1 or full); with it unset, the page
tells you so instead of silently showing nothing.
xr dev --debug turns APP_DEBUG on and sets RUST_BACKTRACE=full
(plus LOG_LEVEL=trace, for maximum detail everywhere at once) for one
session, without editing .env at all - see CLI Reference.
This is the easiest way to always get a real backtrace on a panic.
Never enable APP_DEBUG in production. It’s exactly as dangerous as
Laravel’s own APP_DEBUG=true in production: full error detail -
potentially including query text, file paths, and internal state - goes
straight to whoever’s request triggered it. Every .env.example this
framework generates ships it true for local dev with this warning
attached; double-check it’s false (the default when the var is unset)
before a real deploy.
With APP_DEBUG=false (or unset), the same failures render the plain
default (or your own custom) 404/500 page instead - no detail leaked,
just logged server-side via tracing::error!.
Logging
Every generated app logs through tracing -
larust_support::tracing::info!/warn!/error! are the same macros
AppError’s own 500/panic handling already calls internally. By default
that output goes straight to the terminal (LOG_CHANNEL=stdout, the
implicit default with no .env entry at all) - fine for xr dev and
anything running under a process manager or container runtime that
already captures stdout for you.
# .env
LOG_CHANNEL=stdout # stdout (default) | file | stack
LOG_LEVEL=debug # trace | debug | info | warn | error - leave unset for this framework's own default
LOG_MAX_SIZE=10485760
LOG_KEEP_FILES=5
LOG_CHANNEL=file writes to storage/logs/larust.log instead of the
terminal; LOG_CHANNEL=stack writes to both at once (Laravel’s own
LOG_CHANNEL=stack meaning). Either way, the log file rotates once it
passes LOG_MAX_SIZE bytes (10 MiB by default): the current file becomes
larust.log.1 (shifting any existing .1/.2/… down one generation
first) and a fresh, empty file is started. LOG_KEEP_FILES caps how many
rotated backups survive that shift - anything older is deleted outright;
LOG_KEEP_FILES=0 keeps no backups at all, just the current file.
LOG_LEVEL picks a single level for every crate at once
(trace/debug/info/warn/error) - left unset, verbosity falls back
to this framework’s own existing default (debug when APP_ENV=local,
info otherwise). sqlx/tower_sessions’s own per-query/per-request
debug spans stay capped at warn regardless of LOG_LEVEL - both are
extremely noisy at debug/trace, logging full SQL statements per call.
For anything more targeted than a single level - “everything at info,
except sqlx at debug” - set the standard RUST_LOG
environment variable instead; it’s read before LOG_LEVEL and wins over
it (and over the built-in default) whenever it’s set at all.
Not a full replacement for Laravel’s own single/daily/stack/slack
channel menu - stack here only ever means “stdout and the rotating file
together,” and rotation is size-based only (no daily channel). Sending
an alert (email/Slack) when a rotation actually happens isn’t built in
either.
xr dev’s build-status banner
Worth knowing about separately from request-time errors: while running
xr dev, a small fixed banner appears in any open browser tab the moment
a rebuild starts, and again if that build fails - because otherwise a
request landing on the old, still-running process mid-rebuild gets a
confident, correct-looking response for code that’s already been changed
underneath it, which is a confusing thing to debug if you don’t know it’s
happening. A failed build never takes the site down; the last known-good
version keeps serving until a new build succeeds. See Your First
App.
Next
You’ve now seen the whole request lifecycle - routing, controllers, validation, templates, sessions, and errors. Database covers models, migrations, and relationships next.