Chapter 18

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.

NodeWhat it observesOutput focusWhat it cannot establish
DebugAn ordinary msg actually received by the nodeA selected property, the complete msg, or a JSONata resultNo message does not necessarily mean that an upstream error occurred
CatchA catchable error reported by a node within scopemsg.error and source informationIt does not represent every failure, rejection, or status change
StatusA status event emitted by a node within scopemsg.status; it does not create a payloadStatus text is not the result of processing a message
CompleteThe selected node notifying the runtime that it has finished processing a messageTriggers another flow pathIt 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.

ChannelTypical fieldsHow to interpret it
Ordinary messagemsg.payload, msg.topic, msg._msgidValidate it against the node’s input/output contract
Catch errorerror.message, error.source.id/type/nameRoute first by source and error category; do not write the complete msg to a log
Status eventstatus.text, status.source.id/type/nameA status may reflect a connection or a node-defined condition; test it alongside timing and messages
Complete eventcomplete.source.id/type/name, added to a clone of the original messageApplies 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

  1. 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.”

  2. 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.

  3. Debug the precise field first.

    Do not begin by outputting the complete msg object. Inspect msg.payload or a new msg.testResult first. Give each test a nonsensitive case name, then run the input matrix manually, one case at a time.

  4. 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.

  5. 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: none

Debug: 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.

Version note for 22.0.1/5.0.2: The add-on’s bundled packages include [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.
PracticePurposeCost or risk
One property to the sidebarConfirm its type and a local resultYou must still confirm that the property itself is not sensitive
Complete msg to the sidebarBriefly explore an unknown shapeHigher serialization, copying, and UI rendering costs, with greater exposure
JSONata projection to the sidebarSelect only allowlisted fieldsThe expression itself must be tested for missing values and errors
Output to the runtime logPerform limited diagnostics when the editor is unavailableRetention periods, authorized readers, and centralized-log boundaries differ
Node status displayShow a very short summary or countIts 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;
Performance and privacy: Sending complete high-frequency events to Debug increases message formatting, transmission, and sidebar processing. Enable only the minimum Debug output during the reproduction window, and limit both fields and case counts. When finished, disable it with the node button or remove it, and handle any generated logs according to your data-retention rules.

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.

ObservationCheck firstReason
Function validation rejects inputCatchnode.error(..., msg) creates a catchable error
A node shows connecting/connectedStatusThis is node status and is not necessarily tied to one msg
An ordinary data result is wrongLimited Debug + invariantsNo error or status event may exist
A selected node finishes processingCompleteThe 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.

StatementCan Complete alone establish it?Qualification
The selected node called the completion APIYesThe node implementation defines the exact semantics
Downstream received the selected node’s outputNot necessarilyCompletion and downstream execution have different scopes
Every node in the entire flow has finishedNoThere is no automatic whole-graph join semantic
The external service has permanently completed the workNoThe protocol requires explicit acknowledgement or verification
No Complete event means that the node failedNoThe 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.

  1. First inspect the example as text. Confirm that it contains 12 nodes, that all three Inject nodes use once=false with blank repeat/crontab settings, and that the Function node has no lifecycle hooks, modules, timers, or I/O.
  2. 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.
  3. 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 of msg.complete, not a complete message, and it does not mean that the downstream Debug node has finished.
  4. Immediately restore Complete to d:true after 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 message TEST_ERROR and a source pointing to the same Function in msg.error. The Status Inject makes the status Debug receive fill blue, shape dot, text TEST_STATUS, and a source pointing to that Function in msg.status. Neither path produces ordinary payload output.
Interpretation boundaries: Ordinary output and Complete are independent observation paths. Catch continues to read only 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.

StageTestPass condition
1. Pure transformationManual Inject → pure Function/Change → limited DebugNormal, boundary, and invalid types behave as expected
2. Error contractTargeted Catch → error-projection DebugEach rejected case enters only the expected error path
3. Status/CompleteTargeted Status and Complete nodes, recorded separatelyStatus or node completion is not treated as end-to-end success
4. Architectural boundariesTwo Subflow instances or a Link Call timeoutState-isolation, return, and timeout contracts hold
5. Pre-integration reviewKeep external I/O disabled; review configuration, targets, and recoveryThe change process separately approves entry into the integration environment
6. RegressionCore cases + the current fix’s case + smoke cases for adjacent flowsNo 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, or node.error without 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.text and msg.status.source instead, 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.

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?
Give the compensation path an independent, mutually exclusive Catch scope, and add a bounded error stage or retry count to the message. At the limit, record only minimal diagnostics and stop; do not send the message back to the original entry point. Compensation must not trigger the same external side effect again, and its error text must not include the complete original message.
Status changes repeatedly within a short period. What should the test examine?
Do not capture only the final color. Use limited manual cases to record the status sequence, source, timing, and whether it exceeds the permitted frequency. If a downstream path raises alerts, apply deduplication and throttling first. Status is still not a business-success signal, and jitter must not cause an Action to be resent.
Does Complete mean that the entire flow has finished?
No. It triggers only when the selected node calls the completion API. It does not mean that all downstream work is complete or that an external system has reached its final state. Not every node supports completion.
Why not leave complete-message Debug output enabled?
A complete msg increases sensitive-data exposure and the formatting, transmission, and sidebar workload. Prefer allowlisted properties, enable Debug only during a bounded reproduction window, and disable or remove it afterward.
Can I deploy the example immediately after downloading and importing it?
No. First read the JSON and README offline, then inspect every node in an isolated copy. Catch, Status, and Complete are fixed at d: true; any activation and deployment requires separate approval.
Which regression tests should I run after fixing a Function?
At minimum, run the original core success cases, all existing negative cases, a reproduction of this bug, the invariants, and smoke cases for adjacent input/output shapes. Then validate with the smallest sufficient deployment scope to avoid unnecessary restarts of other flows.