Skip to content

Debugging and reload on save

Firenook reloads your functions when you save, can hold a call at a breakpoint, and keeps every call’s logs and errors. This page shows how to use each while you work on a function.

Firenook watches each codebase’s folder. When you save a file, it waits until a second passes with no further change, then loads that codebase again in a new worker. The terminal says so:

19:47:46 ↻ reloaded 229 ms 3 functions · default

default is the codebase. The watcher leaves out files and folders whose names start with a dot (so .env files too), node_modules, venv, *.log files, and the patterns in the codebase’s ignore list in firebase.json.

A reload that fails keeps the version that loaded before, so your app keeps working while you fix the error:

19:47:49 ✕ reload failed 121 ms default
│ SyntaxError: Unexpected token ';' functions/index.js:20
│ exports.oops = (;
│ ^
│ still running the version loaded at 19:47:46 · open in console

Save the fix and the next reload takes over.

For TypeScript, Firenook runs the compiled JavaScript your package.json points at. Start with --build and Firenook runs your compiler in watch mode beside the emulators, so every save is compiled and reloaded; TypeScript functions has the details. If your sources change but the compiled files do not, the console’s page for the codebase points it out.

In 0.2.0, a save that stops a codebase loading, such as code that throws while it loads or a param with no default and no value, is reported only on the terminal screen. The plain output (--plain, pipes and CI) and the --debug log print nothing about it, and the version loaded before keeps answering, so a call still gets the old behaviour. The screen shows it:

19:36:17 ✕ reload failed 117 ms default
│ Functions codebase could not be analyzed successfully. It may have a syntax or runtime
│ error
│ Error: SHOP_REGION is not set
│ still running the version loaded at 19:35:59 · open in console

When you run with --plain, open the codebase in the console’s Functions page after a save: it says the codebase failed to load, with the error, and that the previous version is still serving. Calling the function you changed shows the same thing.

Press r in the terminal to reload every codebase of yours, or use Reload on a codebase in the console. A reload by hand is how a change to a .env file or .secret.local reaches your functions, since Firenook does not reload on those (environment variables and secrets).

Start with --inspect-functions to open Node’s inspector on each codebase’s worker:

Terminal window
npx firenook emulators:start --inspect-functions

The first worker listens on 127.0.0.1:9229, the next on 9230, and so on. To choose the first port, give it: --inspect-functions=9333. With one codebase, the plain log names its port:

firenook functions: Using debug port 9229 for functions codebase default.
firenook functions: Debugger listening on ws://127.0.0.1:9229/27e0016a-a53d-4dc1-8fa2-1d8dbc90e235

Extensions take the first ports, then your codebases. So with an Extension in firebase.json, the port you give goes to the Extension, and your code gets the next one:

firenook functions: Using debug port 9229 for functions codebase myext.
firenook functions: Using debug port 9230 for functions codebase default. You may need to add manually add this port to your inspector.
firenook functions: Debugger listening on ws://127.0.0.1:9230/fbe26b0c-0897-4cef-a4f0-de7742ee7ef8
firenook extensions[myext]: Debugger listening on ws://127.0.0.1:9229/0b80ac38-04f5-4411-a598-d0bba6e30439

Attach your debugger to the port the log gives default (or your codebase’s name), here 9230.

While the debugger is on, calls have no timeout, so a breakpoint can hold one for as long as you need. Without it, a call stops at the function’s timeoutSeconds.

A reload starts a new worker, and with it a new debugger session on the same port. After a reload, the new worker starts at the next call. Use a debugger that attaches again by itself.

  1. Start the emulators with --inspect-functions.

  2. Open chrome://inspect in Chrome. Under Devices, choose Configure and check that 127.0.0.1:9229 (or your port) is in the list.

  3. Choose Open dedicated DevTools for Node. That window attaches to the worker and attaches again after each reload.

  4. In Sources, open your function’s file, set a breakpoint, and call the function from your app or with npx firenook functions:invoke.

Add an attach configuration, start the emulators with --inspect-functions, then run it from the Run and Debug view:

.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach to Firenook functions",
"address": "127.0.0.1",
"port": 9229,
"restart": true,
"skipFiles": ["<node_internals>/**"]
}
]
}

"restart": true makes VS Code attach again after every reload. With several codebases, add one configuration per port.

Each call is a line: the time, a tick or a cross, the function, how long it took and what called it. The lines your function logged follow it, with structured fields after the message:

19:47:43 ✓ helloWorld 7 ms http · GET / · 200
│ Hello logs! structuredData true
19:47:43 ✕ brokenCheckout 11 ms http · POST / · 500
│ checking out order null
│ Unhandled error TypeError: Cannot read properties of null (reading 'items')
│ at functions/index.js:12:31
│ + 9 more lines · open in console

A failed call shows the error and the first line of the stack that is in your own code. open in console leads to the whole call.

With --plain, or when the output goes to a pipe or CI, Firenook prints the official emulator’s lines instead (the stack trimmed):

firenook functions: Beginning execution of "us-central1-brokenCheckout"
firenook functions: {"order":null,"severity":"INFO","message":"checking out"}
firenook functions: {"severity":"ERROR","message":"Unhandled error TypeError: Cannot read properties of null (reading 'items')\n at ~/demo-shop/functions/index.js:12:31\n ..."}
firenook functions: Finished "us-central1-brokenCheckout" in 12ms

Functions › Invocations in the console lists every call, live. Open one to see how it ended and why, its timing, the request or event it got, the response, its own log lines and the full stack of an error. Replay sends the same request again to the code loaded now; Edit and replay lets you change it first. Firenook keeps the last 2,000 calls.

--debug writes Firenook’s whole log, with times, to .firenook/runs/session-*/firenook-debug.log. In plain mode the start prints the file’s path:

Debug log: ~/demo-shop/.firenook/runs/session-HRKIKS/firenook-debug.log
[INFO] [2026-10-22T11:48:02.206861Z] [functions] functions: Beginning execution of "us-central1-brokenCheckout"
[INFO] [2026-10-22T11:48:02.216076Z] [functions] functions: {"order":null,"severity":"INFO","message":"checking out"}
[INFO] [2026-10-22T11:48:02.219083Z] [functions] functions: Finished "us-central1-brokenCheckout" in 12ms

Attach this file when you report a problem.

Start with the terminal: the error’s message and the line in your code that threw. For the rest, open the call in the console. There you see the whole stack, the request that caused it and the response your app got, and after a fix you can replay the same request.

A call that runs past its timeoutSeconds is stopped, and the caller gets a 500:

firenook functions[us-central1-quote]: Your function timed out after ~2s. To configure this timeout, see
https://firebase.google.com/docs/functions/manage-functions#set_timeout_and_memory_allocation.

See everything the terminal shows in the terminal, or trigger any function by hand with functions:invoke, described with Cloud Functions.