Guide

Debug Scala handlers

See error causes in development or debug mode, keep production opaque, and map Nitro's JavaScript back to Scala.

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: cause is the exception's class, message, cause chain and stack text.
  • ZIO handlers: cause is ZIO's full Cause.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:

FlagSet byWhen
developmentThe plugin, from Nitro's import.meta.devnuxt dev. A production build compiles it to false
debugThe plugin, from runtimeConfig.h34s.debug, read once at startupNUXT_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:

  1. fastLinkJS writes JavaScript and .js.map files to the output directories in your h34s.config.mjs. The demo uses examples/nuxt/server/.scala and examples/nuxt/server/.scala-kyo.
  2. 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.
  3. Start Nuxt with node --enable-source-maps, as in the dev script 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.