Repository: node-red/node-red (js)

Issue:
catch node does not preserve error causes
### Current Behavior

Errors with `cause` option specified when throwing fail to preserve this field when coming out of the `catch` node.

### Expected Behavior

The error should come out as-it-was-thrown.

### Steps To Reproduce

Import the example flow, inject the message using the sole inject node therein and observe the debug window afterwards. The warning messages will show the structure of the error object at the time of throw. The error messages will show the structure of the error object(?) after it was caught.

### Example flow

```
[
    {
        "id": "663431495fb7cf6b",
        "type": "tab",
        "label": "Flow 2",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "81cf2e1edec93a29",
        "type": "function",
        "z": "663431495fb7cf6b",
        "name": "generate errors",
        "func": "try {\n    let foo = [];\n    let bar = foo[1][2];\n} catch (e) {\n    let new_error = new Error(\"error happened\", { cause: e} );\n    node.warn(new_error.message);\n    node.warn(new_error.cause.message);\n    throw new_error;\n}",
        "outputs": 0,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 440,
        "y": 160,
        "wires": []
    },
    {
        "id": "22b4d4576d1c58dc",
        "type": "inject",
        "z": "663431495fb7cf6b",
        "name": "inject now here to start",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": "5",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 200,
        "y": 160,
        "wires": [
            [
                "81cf2e1edec93a29"
            ]
        ]
    },
    {
        "id": "eafe4a6f53bf999d",
        "type": "catch",
        "z": "663431495fb7cf6b",
        "name": "",
        "scope": null,
        "uncaught": false,
        "x": 160,
        "y": 220,
        "wires": [
            [
                "7847ba7ac6d72d1a"
            ]
        ]
    },
    {
        "id": "7847ba7ac6d72d1a",
        "type": "function",
        "z": "663431495fb7cf6b",
        "name": "observe the error",
        "func": "node.error(`${msg.error.message}`);\nnode.error(`${msg.error.cause}`);\n\n\n\nreturn msg;",
        "outputs": 0,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 440,
        "y": 220,
        "wires": []
    }
]
```

### Environment

- Node-RED version: 3.1.7
- Node.js version:20.12.2
- npm version::shrug:
- Platform/OS:Linux
- Browser:Firefox


Pull request:
Pass full error object in Function node and copy over cause property
Fixes #4683

This does two things around error handling

1. preserves the `cause` property of any Error object passed to `node.error(...)`. This is a standard property of Error so reasonable to copy over.
2. updates Function node error handling to pass up the whole error object, not just the message string

The results of this will mean if an error thrown in a Function node includes a cause property it will be present on the message sent by a Catch node.

Patch:
diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js
index 4016c7b90..580b115f0 100644
--- a/packages/node_modules/@node-red/nodes/core/function/10-function.js
+++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js
@@ -374,7 +374,7 @@ module.exports = function(RED) {
                         iniOpt.breakOnSigint = true;
                     }
                 }
-                node.script = new vm.Script(functionText, createVMOpt(node, ""));
+                node.script = vm.createScript(functionText, createVMOpt(node, ""));
                 if (node.fin && (node.fin !== "")) {
                     var finText = `(function () {
                         var node = {
@@ -438,10 +438,9 @@ module.exports = function(RED) {
 
                             //store the error in msg to be used in flows
                             msg.error = err;
-
-                            var line = 0;
-                            var errorMessage;
                             if (stack.length > 0) {
+                                let line = 0;
+                                let errorMessage;
                                 while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
                                     line++;
                                 }
@@ -455,11 +454,13 @@ module.exports = function(RED) {
                                         errorMessage += " (line "+lineno+", col "+cha+")";
                                     }
                                 }
+                                if (errorMessage) {
+                                    err.message = errorMessage
+                                }
                             }
-                            if (!errorMessage) {
-                                errorMessage = err.toString();
-                            }
-                            done(errorMessage);
+                            // Pass the whole error object so any additional properties
+                            // (such as cause) are preserved
+                            done(err);
                         }
                         else if (typeof err === "string") {
                             done(err);
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 dd3d335a2..c4f4e39a2 100644
--- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
+++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
@@ -678,6 +678,9 @@ class Flow {
                 if (logMessage.hasOwnProperty('stack')) {
                     errorMessage.error.stack = logMessage.stack;
                 }
+                if (logMessage.hasOwnProperty('cause')) {
+                    errorMessage.error.cause = logMessage.cause;
+                }
                 targetCatchNode.receive(errorMessage);
                 handled = true;
             });


Test patch:
diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js
index acb693208..371c810d9 100644
--- a/test/nodes/core/function/10-function_spec.js
+++ b/test/nodes/core/function/10-function_spec.js
@@ -390,7 +390,8 @@ describe('function node', function() {
                     msg.should.have.property('level', helper.log().ERROR);
                     msg.should.have.property('id', 'n1');
                     msg.should.have.property('type', 'function');
-                    msg.should.have.property('msg', 'ReferenceError: retunr is not defined (line 2, col 1)');
+                    msg.should.have.property('msg')
+                    msg.msg.message.should.equal('ReferenceError: retunr is not defined (line 2, col 1)');
                     done();
                 } catch(err) {
                     done(err);
@@ -659,7 +660,8 @@ describe('function node', function() {
                 msg.should.have.property('level', helper.log().ERROR);
                 msg.should.have.property('id', name);
                 msg.should.have.property('type', 'function');
-                msg.should.have.property('msg', 'Error: Callback must be a function');
+                msg.should.have.property('msg')
+                msg.msg.message.should.equal('Callback must be a function');
                 done();
             }
             catch (e) {