Debug, Catch, Status, Complete, and Testing Strategies
Observability does not mean printing an entire message. First distinguish ordinary data, catchable errors, node status, and processing-completion events. Then use manual cases with no external I/O to verify both success and failure paths. This chapter uses the precise behavior of Node-RED 5.0.2 to establish a repeatable debugging and regression method.
Observe the right event to avoid false conclusions
A green status beneath a node does not mean that the message it just processed succeeded. An expected payload in Debug does not mean that every downstream node has finished. A Complete event does not mean that all downstream work has finished. These four core nodes observe different events; conflating them creates false signals of success.
| Node | What it observes | Output focus | What it cannot establish |
|---|---|---|---|
| Debug | An ordinary msg actually received by the node | A selected property, the complete msg, or a JSONata result | No message does not necessarily mean that an upstream error occurred |
| Catch | A catchable error reported by a node within scope | msg.error and source information | It does not represent every failure, rejection, or status change |
| Status | A status event emitted by a node within scope | msg.status; it does not create a payload | Status text is not the result of processing a message |
| Complete | The selected node notifying the runtime that it has finished processing a message | Triggers another flow path | It does not establish that all downstream work is complete, and not every node supports it |
This chapter uses only manual Inject nodes, pure Function nodes, and limited Debug nodes for simulation. It does not call Home Assistant, send network requests, read or write files, or use credentials. For the distinction between synchronous return and asynchronous node.send/node.done, revisit Chapter 16: Function Nodes. For architectural boundaries, see Chapter 17.
Four signal channels with explicit scopes
Ordinary messages travel along wires. Errors, statuses, and completions reach matching nodes through runtime event mechanisms. Merely placing Catch, Status, and Complete nodes on the canvas does not automatically monitor the whole deployment. You must understand the boundaries of the same flow tab, selected nodes, propagation into and out of Subflows, and whether a node supports completion.
| Channel | Typical fields | How to interpret it |
|---|---|---|
| Ordinary message | msg.payload, msg.topic, msg._msgid | Validate it against the node’s input/output contract |
| Catch error | error.message, error.source.id/type/name | Route first by source and error category; do not write the complete msg to a log |
| Status event | status.text, status.source.id/type/name | A status may reflect a connection or a node-defined condition; test it alongside timing and messages |
| Complete event | complete.source.id/type/name, added to a clone of the original message | Applies only to a configured node, which must call the completion API |
When the Node-RED runtime creates a Catch message, it adds msg.error. If the original message already has an error property, the existing value is moved to msg._error. Business data should therefore not appropriate the reserved semantics of msg.error. A Status node explicitly does not create payload; downstream nodes must read msg.status rather than carry over payload assumptions from an ordinary data path.
Build a reproducible five-step diagnostic process
- State the observable expectations first.
In one sentence, specify the input, expected output, permitted side effects (none in this chapter), error category, and definition of completion. For example: “A finite numeric input produces twice its value; a nonnumeric input becomes a Catch error with
INVALID_NUMBER.” - Reduce the test to a manual, data-only path.
Copy it to an isolated tab and disable external I/O and operational nodes. Keep Inject manual and prevent it from running at startup. Retain only one entry point, the logic under test, and the required observation points.
- Debug the precise field first.
Do not begin by outputting the complete msg object. Inspect
msg.payloador a newmsg.testResultfirst. Give each test a nonsensitive case name, then run the input matrix manually, one case at a time. - Add Catch, Status, and Complete separately.
Connect each to its own limited Debug node, and select only the node under test. Trigger a known error, known status, and completion one at a time; confirm that you have not mistaken one event for another.
- Record the baseline and deploy only what is necessary.
Save the cases and their expected and actual results, but not complete private messages. Before restoring the production path, review disabled states, external targets, and recovery points. Use the smallest sufficient deployment scope, then run the core regression cases.
Case N1: payload = 4 → result = 8; no Catch
Case N2: payload = 0 → result = 0; no Catch
Case X1: payload = "4" → Catch: INVALID_NUMBER
Case X2: payload missing → Catch: INVALID_NUMBER
Invariant: input msg.topic is unchanged
External I/O: noneDebug: the least data, for the shortest time, at the lowest frequency
A Debug node can display a selected message property, a complete message, or the result of a JSONata expression in the Debug sidebar. It can also write output to the runtime log or show a short value as the node’s status. Its default is msg.payload. The sidebar’s structured view makes objects and arrays easy to expand and can locate nodes on the canvas from source information, but that convenience is not a reason to output all data indefinitely.
A complete msg may contain locations, notification text, Home Assistant entity data, live HTTP objects, or other environment-specific information. Content written to the runtime log leaves the sidebar’s temporary context and may enter centralized collection and retention systems. Stack traces may likewise reveal file paths or sensitive content and must be sanitized before sharing.
[email protected], but the pinned source alone does not prove that it is registered or enabled. Do not claim that stack traces from the 5.0.2 runtime necessarily include source-map mappings. The primary rule is unchanged: limit logs and stack traces, and sanitize them first.| Practice | Purpose | Cost or risk |
|---|---|---|
| One property to the sidebar | Confirm its type and a local result | You must still confirm that the property itself is not sensitive |
| Complete msg to the sidebar | Briefly explore an unknown shape | Higher serialization, copying, and UI rendering costs, with greater exposure |
| JSONata projection to the sidebar | Select only allowlisted fields | The expression itself must be tested for missing values and errors |
| Output to the runtime log | Perform limited diagnostics when the editor is unavailable | Retention periods, authorized readers, and centralized-log boundaries differ |
| Node status display | Show a very short summary or count | Its length is limited, and it cannot replace structured validation |
// Offline Function: creates a minimal diagnostic projection without copying the full input
msg.testResult = {
caseName: String(msg.caseName || "Unnamed case"),
payloadType: typeof msg.payload,
hasTopic: Object.hasOwn(msg, "topic")
};
return msg;
Catch handles the error path; Status reports status events
Catch: capture only catchable errors
A Catch node receives an error only when a node reports it through the runtime’s error mechanism while processing a message. To create a catchable error in a Function node, call node.error(message, msg) with the original message. Calling only node.error(message) does not associate the error with that message for Catch to handle. In general, report JavaScript validation failures explicitly and stop processing; do not emit a success result and an error together.
// Pure-data validation: no network, file, or Home Assistant operation
if (!Number.isFinite(msg.payload)) {
node.error("INVALID_NUMBER", msg);
return;
}
msg.testResult = msg.payload * 2;
return msg;
By default, Catch can capture errors from nodes on the same tab. You can instead select specific nodes or capture only errors that a targeted Catch has not already handled. If an error matches multiple Catch nodes, every match receives it, so multiple error-handling paths may duplicate work. An error inside a Subflow is handled first by a Catch within that Subflow; it propagates to the tab containing the instance only if no internal Catch matches. A third-party node failure, connection condition, empty output, or business-level rejection is not necessarily a catchable error. Check the node’s contract and verify its behavior.
Status: observe statuses that nodes actively publish
A Status node receives status messages published by nodes on the same workspace tab. Its default scope can cover that tab, or you can select individual nodes. Its output contains msg.status.text and the source type/id/name; it does not create a payload. A node showing “connected,” “waiting,” or a color change is reporting only a status emitted by that node—not proof that a particular business message succeeded.
| Observation | Check first | Reason |
|---|---|---|
| Function validation rejects input | Catch | node.error(..., msg) creates a catchable error |
| A node shows connecting/connected | Status | This is node status and is not necessarily tied to one msg |
| An ordinary data result is wrong | Limited Debug + invariants | No error or status event may exist |
| A selected node finishes processing | Complete | The node must support the completion API |
In the downloadable example, Catch and Status are operational observation nodes, so this site’s security rules keep them disabled with d: true. Do not enable every observer in a live flow at once merely to see events; first read the isolation procedure in the next section.
Complete: a node completion API, not completion of the entire path
A Complete node triggers when a selected node tells the runtime, “I have finished processing this message.” Node-RED 1.0 introduced this capability through the node completion API; the node implementation must call done when its synchronous or asynchronous work finishes. Not every node supports it, so the absence of a Complete event does not by itself establish failure.
Complete requires you to select the nodes to monitor, unlike Catch’s default mode for an entire flow. It is useful for observing a node that has no output port but implements completion, or for turning the end of a selected node’s processing into a separate internal control path. It proves only that “the selected node declared completion.” It does not prove that every downstream message previously sent by that node has finished processing, or that an external system has completed the ultimate effect.
| Statement | Can Complete alone establish it? | Qualification |
|---|---|---|
| The selected node called the completion API | Yes | The node implementation defines the exact semantics |
| Downstream received the selected node’s output | Not necessarily | Completion and downstream execution have different scopes |
| Every node in the entire flow has finished | No | There is no automatic whole-graph join semantic |
| The external service has permanently completed the work | No | The protocol requires explicit acknowledgement or verification |
| No Complete event means that the node failed | No | The node may not implement completion |
A Function node’s synchronous path can use return msg directly. Its asynchronous mode normally uses node.send(msg) and then calls node.done() when the work truly ends. Function nodes also have On Start/On Stop lifecycle hooks for initializing or cleaning up resources at deployment and shutdown boundaries. This lifecycle code also needs error, timeout, and cleanup strategies; do not treat it as unbounded background work. This chapter deliberately omits timers and external-I/O examples so that an asynchronous demonstration cannot leave residual work behind.
Run the reproducible Complete exercise in the downloadable example
11-debug-catch-status.json contains exactly one Complete node named “Observe Function completion.” Its scope selects only the core Function node named “Generate a finite diagnostic event.” Complete, Catch, and Status all begin disabled with d:true. Complete connects to its own Debug node, “Inspect complete,” which projects only msg.complete and remains entirely separate from the two Debug nodes that project msg.error and msg.status.
- First inspect the example as text. Confirm that it contains 12 nodes, that all three Inject nodes use
once=falsewith blank repeat/crontab settings, and that the Function node has no lifecycle hooks, modules, timers, or I/O. - Import an isolated copy and enable only Complete temporarily; leave Catch and Status disabled. Click “Manual test: normal output” once. Do not replace it with a startup or scheduled Inject.
- The ordinary Debug node must receive exactly
{"caseName":"normal","doubled":8}. The Complete Debug node must receive exactly{"source":{"id":"b000000000000005","type":"function","name":"Generate a finite diagnostic event"}}. This is a projection ofmsg.complete, not a complete message, and it does not mean that the downstream Debug node has finished. - Immediately restore Complete to
d:trueafter the test. To test the other two paths, enable only the corresponding observer, one at a time. The Catch Inject makes the error Debug receive messageTEST_ERRORand a source pointing to the same Function inmsg.error. The Status Inject makes the status Debug receive fillblue, shapedot, textTEST_STATUS, and a source pointing to that Function inmsg.status. Neither path produces ordinary payload output.
msg.error, and Status continues to read only msg.status. Do not enable all operational observers at once, and do not treat completion as end-to-end acknowledgement.Staged testing, mocks, invariants, and negative cases
Flow testing does not need to begin with a live installation. First use a mock message to fix the input shape, then gradually introduce Context, Subflow, or Link boundaries. A mock is not intended to reproduce every piece of environmental data; it proves the logic with the fewest necessary fields. Unknown fields, missing values, boundary values, and incorrect types must each become explicit test cases.
| Stage | Test | Pass condition |
|---|---|---|
| 1. Pure transformation | Manual Inject → pure Function/Change → limited Debug | Normal, boundary, and invalid types behave as expected |
| 2. Error contract | Targeted Catch → error-projection Debug | Each rejected case enters only the expected error path |
| 3. Status/Complete | Targeted Status and Complete nodes, recorded separately | Status or node completion is not treated as end-to-end success |
| 4. Architectural boundaries | Two Subflow instances or a Link Call timeout | State-isolation, return, and timeout contracts hold |
| 5. Pre-integration review | Keep external I/O disabled; review configuration, targets, and recovery | The change process separately approves entry into the integration environment |
| 6. Regression | Core cases + the current fix’s case + smoke cases for adjacent flows | No unexpected message shapes, errors, or side effects |
Define invariants instead of comparing only payloads
- Input
msg.topic,msg._msgid, and other designated metadata are not overwritten unexpectedly. - An error case is not also sent to the success output.
- Each input produces no more outputs than the contract permits and is not processed repeatedly because multiple Catch nodes match.
- Context keys and their contents remain bounded; state after redeployment matches the design.
- Every timeout, retry, and buffer has an upper bound; the mocks in this chapter create no queues, timers, or external I/O.
Negative-case checklist
At minimum, test a missing payload, null, a string in place of a number, NaN/infinite values, values outside the permitted range, extra fields, duplicate messages, and incorrect ordering. For Link Call, also test a missing return; for Subflow, interleave two instances; for Catch, test a pre-existing msg.error; for Status, test downstream code that incorrectly reads payload; and for Complete, test an unselected node and a node that does not support completion.
Do not use a visual impression of the Debug sidebar as your only evidence. Put case tables, expected fields, and result summaries—without environment data—in the change record. Real secrets, complete messages, stack traces, and runtime logs do not belong in general-purpose tickets.
Deployment scope and regression scope
Node-RED 5.0.2 provides three deployment scopes: Full, Modified Flows, and Modified Nodes. Full restarts every node; Modified Flows restarts flows that contain changed nodes; Modified Nodes restarts only modified nodes. A smaller scope reduces unrelated restarts but does not automatically guarantee safety. First determine whether the change affects Subflow instances, users of configuration nodes, Context, or shared paths; then choose the smallest sufficient scope.
Before each deployment, record a recoverable version and inspect startup Inject nodes, schedules, listeners, and nodes with external side effects. After deployment, run one pure-data smoke case first, followed by regression tests for the current fix and adjacent flows. If a configuration-node or shared-Subflow change affects multiple flows, do not omit part of the affected scope merely to select the smallest label.
Troubleshooting
- Catch does not receive a Function error: Confirm that the Function calls
node.error("category", msg)and then stops emitting output, and that the Catch scope includes that Function. Do not assume that a thrown error, a string warning, no output, ornode.errorwithout msg will all be captured in the same way. - The same error is handled twice: Check whether it matches both a targeted Catch and another Catch. Node-RED delivers an error to every matching Catch node. Make their responsibilities mutually exclusive, or use the “not already caught” mode as the final line of defense.
- Payload downstream of Status is undefined: A Status node does not create a payload. Read
msg.status.textandmsg.status.sourceinstead, and keep the status diagnostic path separate from the ordinary data path. - Complete does not fire: Confirm that Complete has selected the node under test and that the node implements the completion API. Do not add a Delay or declare failure merely because no event arrived. First consult the pinned-version node documentation and verify a minimal case.
- Complete fires, but later work has not finished: This is a scope error. Complete means only that the selected node declared completion; it is not a downstream barrier. If you require end-to-end completion, define a final confirmation signal or a bounded join contract.
- The editor slows down after complete-message debugging is enabled: First disable Debug with the node button, reduce its output to one property, lower the test frequency, and clear unneeded sidebar messages. Do not increase the heap instead of limiting message size and the number of observations.
Pinned sources and official documentation
Exact commits provide the baseline for specific node semantics. The official documentation provides general operating guidance. If the rolling documentation changes, this chapter continues to use the pinned Node-RED 5.0.2 and Add-on 22.0.1 implementations as its basis.
- Pinned Node-RED 5.0.2 commit: Debug, Catch, Status, Complete, the Function lifecycle, the runtime, and deployment scopes.
- Pinned Home Assistant Community App: Node-RED 22.0.1 commit: bundled dependencies and execution-wrapper boundaries.
- Official Node-RED documentation: Handling errors.
- Official Node-RED documentation: Catchable errors.
- Official Node-RED documentation: Node status.
- Official Node-RED documentation: Writing Functions.
This site’s offline, safety-constrained example is 11-debug-catch-status.json. Three manual Inject nodes use msg.mode to select the normal path, the Catch test, or the bounded Status test. Separate Debug nodes for ordinary, error, status, and completion events display only payload, error, status, and complete, respectively. Read the README in the same directory first. Catch, Status, and Complete remain disabled with d: true and may be enabled only briefly, one path at a time, in an isolated copy.
FAQ
How do I prevent an error loop if the Catch compensation path itself fails?
Status changes repeatedly within a short period. What should the test examine?
Does Complete mean that the entire flow has finished?
Why not leave complete-message Debug output enabled?
Can I deploy the example immediately after downloading and importing it?
d: true; any activation and deployment requires separate approval.