Debugging Rust handlers
This page shows how to find the cause of a bug in a load, an action or an endpoint. It lists the tools, from the quickest to the most exact. To find slow code, see Profiling.
The dev error page
When a handler panics under wisp dev, the server catches the panic and answers with a 500. The dev error page shows:
- the panic message
- the place of the panic as
file:line:column - a code frame: the line that panicked and two lines above and below it, with line numbers and a
^under the column - the request: its method, path and query string
panic at src/routes/posts/[slug]/+page.rs:12:32: called `Option::unwrap()` on a `None` value
--> src/routes/posts/[slug]/+page.rs:12:32
|
10 | pub async fn load(cx: &mut Cx) -> Result<Page> {
11 | let slug = cx.param("slug");
12 | let post = POSTS.get(slug).unwrap();
| ^
13 | Ok(Page { post })
14 | }
The same text opens in the dev dialog of the browser. Click the file:line link to open the line in your editor.
A panic in the code that a template makes shows the .wisp file and line, with the frame of the template. See Template errors.
The frame follows these rules:
- It shows only under
wisp dev(a debug build with dev mode on). A release build, and any server withWISP_DEV=off, answers with a plain 500 and shows no message and no source. - It reads only files inside the app folder. It reads a relative path from the app folder first, then from the folders above it. If both hold a file with that path, it shows the file of the app folder. A panic in the standard library or in a dependency has the message and the place but no frame.
- It skips a file that is larger than 1 MiB. A source line shows its first 160 characters.
- Wisp escapes all of the text, so a source line with HTML in it shows as text.
A handler that returns an Err (not a panic) shows the status and the message on the same page, with the request. Use error(status, message) with a message that names the cause.
Print values
dbg! prints the file, the line, the expression and its value to the standard error of the app. wisp dev shows it in the terminal where it runs.
let post = dbg!(POSTS.get(slug));
println! and eprintln! show there too. A dbg! moves its value, so pass a reference or a clone when you need the value after it. Delete each dbg! before you commit. cargo clippy warns about one with the dbg_macro lint when you enable it.
Backtraces
Wisp keeps the panic hook quiet for a handler panic, because the 500 reports it once. Set RUST_BACKTRACE=1 to get the standard panic report with its backtrace in the terminal, as well.
RUST_BACKTRACE=1 wisp dev
Use RUST_BACKTRACE=full for every frame. Backtraces name functions of the app because a dev build has debug information.
Logs
WISP_LOG=jsonwrites one JSON line per request, with the status and time. See Logs, metrics and traces.- Wisp does not read
RUST_LOG. If the app uses thetracingcrate or thelogcrate, its own subscriber reads that variable. - Set
OTEL_EXPORTER_OTLP_ENDPOINTto send the spans of each request to a trace tool. - Under
wisp dev, pressAlt+Shift+Wfor the devtools: the route, its params and server values, and theServer-Timingof each phase.
wisp dev prints each request in the terminal. A 5xx adds its cause below the request, and a panic adds its code frame.
Attach a debugger
wisp dev runs a copy of your app, .wisp/run/app-<pid>-<slot> (with .exe on Windows), so that cargo build can replace the original. There are two ways to debug it.
Run the binary yourself. Build with cargo build, then run the debugger from the app folder, because a dev build reads static/ and .wisp/ from its working directory:
cargo build
lldb -- target/debug/my-app # or: gdb --args target/debug/my-app
(lldb) breakpoint set --file +page.rs --line 12
(lldb) run
Outside wisp dev the app has no parent process to follow. Set PORT if the default port is in use.
Attach to the running app. Find the process id in .wisp/run or in your process list, then attach:
lldb -p <pid>
On Windows, attach WinDbg or the debugger of your editor to app-<pid>-<slot>.exe. In VS Code, the CodeLLDB extension can run the first way from its debug configuration.
A breakpoint stops the whole server thread, so the browser waits. When wisp dev rebuilds, it starts a new copy and stops the old one, and the debugger detaches. Attach again to the new process.
Common causes
- A 500 with a message about
unwraporexpect. The code frame names the call. Replace it with.or_404(),.or_status(400)or?. - A panic with an index out of range. The frame names the index expression. Print the index and the length with
dbg!. - A hang, not a panic. A blocking call in a handler holds the thread. The dev log of the request names a hold of 100 ms or more.
- A handler that answers but shows wrong data. Print the input with
dbg!first, then the value after each step.
Is This Page Useful?