From 61d0e29269a98498f0c45357839f819539b891cc Mon Sep 17 00:00:00 2001
From: GogoVega <92022724+GogoVega@users.noreply.github.com>
Date: Sun, 26 Oct 2025 18:46:08 +0100
Subject: [PATCH 1/6] WIP: Allow catch node to catch any error from any node
---
.../@node-red/nodes/core/common/25-catch.html | 106 ++++++++++++++----
.../@node-red/nodes/core/common/25-catch.js | 11 +-
.../nodes/locales/en-US/messages.json | 5 +-
.../@node-red/nodes/locales/fr/messages.json | 11 +-
.../@node-red/runtime/lib/flows/Flow.js | 6 +
.../@node-red/runtime/lib/nodes/Node.js | 7 +-
6 files changed, 112 insertions(+), 34 deletions(-)
diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.html b/packages/node_modules/@node-red/nodes/core/common/25-catch.html
index 4b92c9758..23cf54c2a 100644
--- a/packages/node_modules/@node-red/nodes/core/common/25-catch.html
+++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.html
@@ -12,6 +12,14 @@
+
+
+
+
+
+
+
+
@@ -30,9 +38,11 @@
category: 'common',
color:"#e49191",
defaults: {
- name: {value:""},
- scope: {value:null, type:"*[]"},
- uncaught: {value:false}
+ name: { value: "" },
+ scope: { value: null, type: "*[]" },
+ uncaught: { value: false },
+ anyError: { value: false },
+ includeConfig: { value: false }
},
inputs:0,
outputs:1,
@@ -82,27 +92,53 @@
});
var dirList = $("#node-input-catch-target-container-div").css({width: "100%", height: "100%"})
.treeList({multi:true}).on("treelistitemmouseover", function(e, item) {
- item.node.highlighted = true;
- item.node.dirty = true;
- RED.view.redraw();
+ if (item.node) {
+ item.node.highlighted = true;
+ item.node.dirty = true;
+ RED.view.redraw();
+ }
}).on("treelistitemmouseout", function(e, item) {
- item.node.highlighted = false;
- item.node.dirty = true;
- RED.view.redraw();
+ if (item.node) {
+ item.node.highlighted = false;
+ item.node.dirty = true;
+ RED.view.redraw();
+ }
})
- var candidateNodes = RED.nodes.filterNodes({z:node.z});
- var allChecked = true;
- var items = [];
- var nodeItemMap = {};
+
+ const flowItems = [];
+ const flowItemMap = {};
+ let activeWorkspace = RED.nodes.workspace(node.z);
+ activeWorkspace = activeWorkspace || RED.nodes.subflow(node.z);
+ flowItemMap[activeWorkspace.id] = {
+ id: activeWorkspace.id,
+ class: "red-ui-palette-header",
+ label: activeWorkspace.label || activeWorkspace.name || activeWorkspace.id,
+ expanded: true,
+ children: []
+ };
+ flowItemMap.global = {
+ id: "global",
+ class: "red-ui-palette-header",
+ label: this._("catch.label.globalConfig"),
+ expanded: true,
+ children: []
+ };
+
+ const nodeItemMap = {};
+ const configNodeSet = new Set();
+ const candidateNodes = RED.nodes.filterNodes({ z: node.z });
+ RED.nodes.eachConfig((cn) => {
+ // Add config nodes with the same or global scope
+ if (cn.z === node.z || !cn.z) {
+ configNodeSet.add(cn);
+ candidateNodes.push(cn);
+ }
+ });
candidateNodes.forEach(function(n) {
- if (n.id === node.id) {
+ if (n.id === node.id || n.id === "global-config") {
return;
}
- var isChecked = scope.indexOf(n.id) !== -1;
-
- allChecked = allChecked && isChecked;
-
var nodeDef = RED.nodes.getType(n.type);
var label;
var sublabel;
@@ -119,21 +155,42 @@
if (!nodeDef || !label) {
label = n.type;
}
+ const isChecked = scope.indexOf(n.id) !== -1;
+ const isConfigNode = (nodeDef && nodeDef.category === "config") || (!nodeDef && !n.x && !n.y);
nodeItemMap[n.id] = {
node: n,
label: label,
sublabel: sublabel,
selected: isChecked,
- checkbox: true
+ checkbox: true,
+ icon: isConfigNode ? "fa fa-cog" : ""
};
- items.push(nodeItemMap[n.id]);
+ if (!n.z && isConfigNode) {
+ flowItemMap.global.children.push(nodeItemMap[n.id]);
+ } else {
+ flowItemMap[activeWorkspace.id].children.push(nodeItemMap[n.id]);
+ }
+ });
+
+ flowItems.push(flowItemMap[activeWorkspace.id], flowItemMap.global);
+
+ $("#node-input-includeConfig").on("change", function () {
+ const checked = $(this).prop("checked");
+ if (checked) {
+ dirList.treeList('data', flowItems);
+ } else {
+ // Remove config nodes from the list
+ const filteredItems = flowItemMap[activeWorkspace.id].children
+ .filter((e) => !configNodeSet.has(e.node));
+ dirList.treeList('data', filteredItems);
+ }
});
- dirList.treeList('data',items);
$("#node-input-catch-target-select").on("click", function(e) {
e.preventDefault();
var preselected = dirList.treeList('selected').map(function(n) {return n.node.id});
RED.tray.hide();
+ // TODO: extend selection to config nodes too
RED.view.selectNodes({
selected: preselected,
onselect: function(selection) {
@@ -169,6 +226,13 @@
$(".node-input-target-row").hide();
$(".node-input-uncaught-row").show();
}
+ if (scope === "group") {
+ $(".node-input-anyError-row").hide();
+ $(".node-input-includeConfig-row").hide();
+ } else {
+ $(".node-input-anyError-row").show();
+ $(".node-input-includeConfig-row").show();
+ }
node._resize();
});
if (this.scope === null) {
diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.js b/packages/node_modules/@node-red/nodes/core/common/25-catch.js
index 5ed525c36..a343451b9 100644
--- a/packages/node_modules/@node-red/nodes/core/common/25-catch.js
+++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.js
@@ -14,19 +14,20 @@
* limitations under the License.
**/
-module.exports = function(RED) {
+module.exports = function (RED) {
"use strict";
function CatchNode(n) {
- RED.nodes.createNode(this,n);
- var node = this;
+ RED.nodes.createNode(this, n);
this.scope = n.scope;
this.uncaught = n.uncaught;
- this.on("input",function(msg, send, done) {
+ this.anyError = n.anyError;
+ this.includeConfig = n.includeConfig;
+ this.on("input", function (msg, send, done) {
send(msg);
done();
});
}
- RED.nodes.registerType("catch",CatchNode);
+ RED.nodes.registerType("catch", CatchNode);
}
diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json
index 5c9c99e75..c19409a40 100644
--- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json
+++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json
@@ -100,7 +100,10 @@
"label": {
"source": "Catch errors from",
"selectAll": "select all",
- "uncaught": "Ignore errors handled by other Catch nodes"
+ "uncaught": "Ignore errors handled by other Catch nodes",
+ "anyError": "Allow any error other than message processing",
+ "includeConfig": "Catch errors from config nodes too",
+ "globalConfig": "Global config nodes"
},
"scope": {
"all": "all nodes",
diff --git a/packages/node_modules/@node-red/nodes/locales/fr/messages.json b/packages/node_modules/@node-red/nodes/locales/fr/messages.json
index e4c2d49ab..e46bf0c77 100644
--- a/packages/node_modules/@node-red/nodes/locales/fr/messages.json
+++ b/packages/node_modules/@node-red/nodes/locales/fr/messages.json
@@ -98,14 +98,17 @@
"catchNodes": "catch : __number__",
"catchUncaught": "catch : non capturé",
"label": {
- "source": "Détecter les erreurs de",
+ "source": "Capturer les erreurs ",
"selectAll": "Tout sélectionner",
- "uncaught": "Ignorer les erreurs gérées par les autres noeuds Catch"
+ "uncaught": "Ignorer les erreurs gérées par les autres noeuds Catch",
+ "anyError": "Autoriser toute erreur autre que le traitement des messages",
+ "includeConfig": "Capturer également les erreurs des noeuds de configuration",
+ "globalConfig": "Noeuds de configuration globale"
},
"scope": {
- "all": "tous les noeuds",
+ "all": "de tous les noeuds",
"group": "dans le même groupe",
- "selected": "noeuds sélectionnés"
+ "selected": "des noeuds sélectionnés"
}
},
"status": {
diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
index 02b976a32..05a245d6c 100644
--- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
+++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
@@ -620,9 +620,15 @@ class Flow {
handled = node.users[userNode]._flow.handleError(node,logMessage,msg,node.users[userNode]) || handled;
}
}
+ } else if (this.id === 'global') {
+ // TODO: how to trigger Catch nodes? (activeFlows)
} else {
const candidateNodes = [];
this.catchNodes.forEach(targetCatchNode => {
+ if (msg === null && !targetCatchNode.anyError) {
+ // Skip if the catch node only accepts errors produced by message processing
+ return;
+ }
if (targetCatchNode.g && targetCatchNode.scope === 'group' && !reportingNode.g) {
// Catch node inside a group, reporting node not in a group - skip it
return
diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js
index 0b1ed349b..4d66c7667 100644
--- a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js
+++ b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js
@@ -559,9 +559,10 @@ Node.prototype.error = function(logMessage,msg) {
if (typeof logMessage != 'boolean') {
logMessage = logMessage || "";
}
- var handled = false;
- if (this._flow && msg && typeof msg === 'object') {
- handled = this._flow.handleError(this,logMessage,msg);
+ let handled = false;
+ const shouldHandle = (msg && typeof msg === 'object') || msg === undefined;
+ if (this._flow && shouldHandle) {
+ handled = this._flow.handleError(this, logMessage, msg || null);
}
if (!handled) {
log_helper(this, Log.ERROR, logMessage);
From 9e3f95ecadb084cd0f51f536a1310a401fb5547b Mon Sep 17 00:00:00 2001
From: GogoVega <92022724+GogoVega@users.noreply.github.com>
Date: Wed, 29 Oct 2025 12:38:46 +0100
Subject: [PATCH 2/6] Catch errors from config nodes
---
.../@node-red/runtime/lib/flows/Flow.js | 18 +++++++++++++++++-
.../@node-red/runtime/lib/flows/index.js | 1 +
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
index 05a245d6c..09dff8f50 100644
--- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
+++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
@@ -26,6 +26,9 @@ let Subflow;
let Log;
let Group;
+/** @type {Record} */
+let activeFlows;
+
let nodeCloseTimeout = 15000;
let asyncMessageDelivery = true;
@@ -621,14 +624,24 @@ class Flow {
}
}
} else if (this.id === 'global') {
- // TODO: how to trigger Catch nodes? (activeFlows)
+ for (const flow of Object.values(activeFlows)) {
+ if (flow.id === 'global') {
+ continue;
+ }
+ handled = flow.handleError(node, logMessage, msg) || handled;
+ }
} else {
const candidateNodes = [];
+ const isConfigNode = !!node._flow.flow.configs[node.id];
this.catchNodes.forEach(targetCatchNode => {
if (msg === null && !targetCatchNode.anyError) {
// Skip if the catch node only accepts errors produced by message processing
return;
}
+ if (isConfigNode && !targetCatchNode.includeConfig) {
+ // Skip if the catch node only accepts errors produced by non config nodes
+ return;
+ }
if (targetCatchNode.g && targetCatchNode.scope === 'group' && !reportingNode.g) {
// Catch node inside a group, reporting node not in a group - skip it
return
@@ -859,6 +872,9 @@ module.exports = {
Subflow = require("./Subflow");
Group = require("./Group").Group
},
+ setActiveFlows: function (flows) {
+ activeFlows = flows;
+ },
create: function(parent,global,conf) {
return new Flow(parent,global,conf)
},
diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js
index f21bd56f9..28a4c03ed 100644
--- a/packages/node_modules/@node-red/runtime/lib/flows/index.js
+++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js
@@ -70,6 +70,7 @@ function init(runtime) {
typeEventRegistered = true;
}
Flow.init(runtime);
+ Flow.setActiveFlows(activeFlows);
flowUtil.init(runtime);
}
From cee3fd8395ddf5d491a99720a1d934b579542a3f Mon Sep 17 00:00:00 2001
From: GogoVega <92022724+GogoVega@users.noreply.github.com>
Date: Wed, 29 Oct 2025 12:38:59 +0100
Subject: [PATCH 3/6] Temporary fix for unit tests
---
test/nodes/core/function/10-function_spec.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js
index 6a04547f4..ba7c22707 100644
--- a/test/nodes/core/function/10-function_spec.js
+++ b/test/nodes/core/function/10-function_spec.js
@@ -69,7 +69,8 @@ describe('function node', function() {
});
it('should do something with the catch node', function(done) {
- var flow = [{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);"},{"id":"goodNode","type":"helper"},{"id":"badNode","type":"helper"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]]}];
+ // TODO: global flow should not contain these nodes
+ var flow = [{"id":"flow","type":"tab"},{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);","z":"flow"},{"id":"goodNode","type":"helper","z":"flow"},{"id":"badNode","type":"helper","z":"flow"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]],"anyError":true,"includeConfig":true,"z":"flow"}];
var catchNodeModule = require("nr-test-utils").require("@node-red/nodes/core/common/25-catch.js")
helper.load([catchNodeModule, functionNode], flow, function() {
var funcNode = helper.getNode("funcNode");
From c2bd5053cfdaa2dcb978dba9fe0952ade5511c03 Mon Sep 17 00:00:00 2001
From: GogoVega <92022724+GogoVega@users.noreply.github.com>
Date: Wed, 29 Oct 2025 14:42:40 +0100
Subject: [PATCH 4/6] Fix unit tests
---
packages/node_modules/@node-red/runtime/lib/flows/Flow.js | 2 +-
test/nodes/core/function/10-function_spec.js | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
index 09dff8f50..1a3edc595 100644
--- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
+++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
@@ -632,7 +632,7 @@ class Flow {
}
} else {
const candidateNodes = [];
- const isConfigNode = !!node._flow.flow.configs[node.id];
+ const isConfigNode = !!node._flow?.flow.configs[node.id];
this.catchNodes.forEach(targetCatchNode => {
if (msg === null && !targetCatchNode.anyError) {
// Skip if the catch node only accepts errors produced by message processing
diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js
index ba7c22707..46159d545 100644
--- a/test/nodes/core/function/10-function_spec.js
+++ b/test/nodes/core/function/10-function_spec.js
@@ -69,8 +69,7 @@ describe('function node', function() {
});
it('should do something with the catch node', function(done) {
- // TODO: global flow should not contain these nodes
- var flow = [{"id":"flow","type":"tab"},{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);","z":"flow"},{"id":"goodNode","type":"helper","z":"flow"},{"id":"badNode","type":"helper","z":"flow"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]],"anyError":true,"includeConfig":true,"z":"flow"}];
+ var flow = [{"id":"flow","type":"tab"},{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);","x":0,"y":0,"z":"flow"},{"id":"goodNode","type":"helper","z":"flow"},{"id":"badNode","type":"helper","z":"flow"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]],"z":"flow"}];
var catchNodeModule = require("nr-test-utils").require("@node-red/nodes/core/common/25-catch.js")
helper.load([catchNodeModule, functionNode], flow, function() {
var funcNode = helper.getNode("funcNode");
From 59d8568af0f206794a6604077a45ba60c3a33b34 Mon Sep 17 00:00:00 2001
From: GogoVega <92022724+GogoVega@users.noreply.github.com>
Date: Thu, 30 Oct 2025 12:43:33 +0100
Subject: [PATCH 5/6] Highlight config nodes on mouse over
---
.../@node-red/nodes/core/common/25-catch.html | 21 +++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.html b/packages/node_modules/@node-red/nodes/core/common/25-catch.html
index 23cf54c2a..ca4c584c8 100644
--- a/packages/node_modules/@node-red/nodes/core/common/25-catch.html
+++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.html
@@ -90,18 +90,27 @@
}
}
});
+ const sidebar = $("#red-ui-sidebar-config-category-global");
var dirList = $("#node-input-catch-target-container-div").css({width: "100%", height: "100%"})
.treeList({multi:true}).on("treelistitemmouseover", function(e, item) {
if (item.node) {
- item.node.highlighted = true;
- item.node.dirty = true;
- RED.view.redraw();
+ if (!item.node.x && !item.node.y) {
+ sidebar.find(".red-ui-palette-node_id_" + item.node.id).children("div").addClass("highlighted");
+ } else {
+ item.node.highlighted = true;
+ item.node.dirty = true;
+ RED.view.redraw();
+ }
}
}).on("treelistitemmouseout", function(e, item) {
if (item.node) {
- item.node.highlighted = false;
- item.node.dirty = true;
- RED.view.redraw();
+ if (!item.node.x && !item.node.y) {
+ sidebar.find(".red-ui-palette-node_id_" + item.node.id).children("div").removeClass("highlighted");
+ } else {
+ item.node.highlighted = false;
+ item.node.dirty = true;
+ RED.view.redraw();
+ }
}
})
From 5085558cb2dd1f367edf17b91cbf1e61d2bbf799 Mon Sep 17 00:00:00 2001
From: GogoVega <92022724+GogoVega@users.noreply.github.com>
Date: Thu, 30 Oct 2025 12:44:06 +0100
Subject: [PATCH 6/6] Ensure global-config is not selectable
---
packages/node_modules/@node-red/nodes/core/common/25-catch.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.html b/packages/node_modules/@node-red/nodes/core/common/25-catch.html
index ca4c584c8..2b2f43ca6 100644
--- a/packages/node_modules/@node-red/nodes/core/common/25-catch.html
+++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.html
@@ -145,7 +145,7 @@
}
});
candidateNodes.forEach(function(n) {
- if (n.id === node.id || n.id === "global-config") {
+ if (n.id === node.id || n.type === "global-config") {
return;
}
var nodeDef = RED.nodes.getType(n.type);