Debug Scala handlers
h34s keeps production errors opaque and shows full diagnostics while you develop. You do not add a debug flag to your handlers: the h34s Nitro plugin decides, and requests can never change its decision.
Install the npm Nuxt module first. It registers the plugin, the development loader and the source-map integration.
What clients see
An unexpected failure is any Throwable that is not an approved SafeError: a thrown exception, a panic, a converter defect, a non-safe ZIO failure, or a ZIO defect. With the default options, the client gets:
{ "errors": [{ "message": "Internal server error" }] }
with status 500. When diagnostics are on, the same entry carries the cause:
{
"errors": [{
"message": "Internal server error",
"debug": { "cause": "java.lang.IllegalStateException: ...\n\tat ..." }
}]
}
- Kyo handlers:
causeis the exception's class, message, cause chain and stack text. - ZIO handlers:
causeis ZIO's fullCause.prettyPrint, with ZIO traces and Scala source lines.
SafeError entries never carry debug. Their status and messages are the same in every mode. With ErrorEnvelope.SingleOrList, the one entry is under "error" instead of "errors".
The server logs every unexpected failure as Unhandled h34s handler failure with the same details, in every mode.
When diagnostics are on
Diagnostics are on when the request's development flag or debug flag is the boolean true:
| Flag | Set by | When |
|---|---|---|
development | The plugin, from Nitro's import.meta.dev | nuxt dev. A production build compiles it to false |
debug | The plugin, from runtimeConfig.h34s.debug, read once at startup | NUXT_H34S_DEBUG=true at runtime, or runtimeConfig: { h34s: { debug: true } } in nuxt.config.ts |
The Nuxt module declares runtimeConfig.h34s.debug: false, which is why NUXT_H34S_DEBUG can override it. If your nuxt.config.ts already sets runtimeConfig.h34s.debug, your value wins. Nuxt's own debug option does not turn on h34s debug mode: Nuxt resolves it to an object, and the DEBUG environment variable of a build machine can set it. Restart the server after you change the variable.
Without the plugin, both flags are false and every failure is opaque.
Keep debug mode off in production. Diagnostics send exception messages and stack text to anyone who can trigger a failure. Libraries can put connection details, SQL text or file paths in exception messages. Use debug mode only on a server that only you can reach, and turn it off again. If you turned it on by mistake, unset NUXT_H34S_DEBUG (or set it to false) and restart.
These flags are tested with a stubbed Nitro runtime (packages/nuxt/test) and with fake plugin records in the Scala suites. In a production build of the demo (macOS x86_64), an unexpected failure was opaque without the variable and carried debug.cause with NUXT_H34S_DEBUG=true; the database password did not appear in either response.
Map JavaScript back to Scala
During nuxt dev, Node loads the linked Scala.js modules directly and reads their source maps:
fastLinkJSwrites JavaScript and.js.mapfiles to the output directories in yourh34s.config.mjs. The demo usesexamples/nuxt/server/.scalaandexamples/nuxt/server/.scala-kyo.- The module's development loader keeps those outputs out of Nitro's development bundle and imports them as native files. Nitro watches the directories and replaces its worker after you relink.
- Start Nuxt with
node --enable-source-maps, as in thedevscript of npm tooling. Node then maps stack frames to Scala files and lines.
Enable source maps in your Scala.js build:
Compile / fastLinkJS / scalaJSLinkerConfig ~= (_.withSourceMap(true)),
Keep each .js file next to its .js.map. The package also has a source-map helper that composes Scala.js maps through Nitro/Rollup. It embeds your project's Scala sources, from the application root and from the config's sbtRoot, and never downloads dependency sources.
Attach a debugger
Start Nuxt with the inspector from your application root:
pnpm dev --inspect
Attach a Node debugger to the address Nuxt prints, then open the original Scala file in Sources. Handlers run in Node, so use the server's inspector session, not the browser page. Source maps give locations and source views. They do not give Scala expression evaluation or a JVM-style debugger. Breakpoint stepping has not been verified.
Rebuild after editing
Run pnpm run scala:fast after a Scala change, or keep pnpm run scala:watch running. Wait until linking finishes and Nitro restarts its worker before you send the next request. If a stack frame looks stale, check that linking succeeded and the generated files changed.
Checks
node --test scripts/*.test.mjs packages/nuxt/test/*.test.mjs
These Node tests pass on macOS x86_64 with Node 24.19.0 (see compatibility). They cover the development resolver, Rollup-to-Node source maps, map composition, the Nitro plugin's records and flags, the debug flag merge in the Nuxt module, and the close hook. The Scala suites cover opaque 500s, development and debug diagnostics, Kyo stack text and ZIO Cause.prettyPrint with Scala file names, and that SafeError entries never carry debug.
On this branch (macOS x86_64, Node 24.19.0, Nuxt 4.3.1), pnpm dev after pnpm run scala:fast answered /api/notes without a database with a 500 whose debug.cause had Scala stack frames and no password, and a Kyo handler that threw a JavaScript TypeError had its stack mapped to RequestLifecycle.scala and KHandler.scala. scripts/check-npm-nuxt.mjs checked development diagnostics, source maps and a relink reload with the packed module on a synthetic project. Relinking the real Scala.js example under nuxt dev and breakpoint stepping have not been checked.