JSONata interprets message differently from debug node

SYSTEM
Raspi 3B+ running recently updated bookworm
NR 4.0.9
nodejs 20.18.3

Problem
I have a case where JSONata in a change node is producing (to an element of an object) a different value to that displayed in a debug node, or displayed for the object when written to a Global context data.
In this case the object is msg.telegram.error and is displayed as a string "AggregateError", but in a JSONata expression which appends msg.telegram.error to a string, it is displayed as an object {"code":"ETIMEDOUT"}. Nothing else shows there is a msg.telegram.error.code in the message, in fact a meessage element can't be a string and a object at the same time.

I did try to copy to msg.telegram.error to msg.errorT in the change node but no difference, unless I ticked DeepCopy, then ErrorT was displayed empty object in debug node.
I isolated the code into my sandpit and got a copy of msg.telegram by saving it to a globalContext variable telegramTest and saw no change in behaviour.
global.telegramTest is (as displayed in Context Data sidepane)

{"isOnline":false,"error":"AggregateError"}

and sandpit flow is

[
    {
        "id": "aa47566793e591e0",
        "type": "ping",
        "z": "db3e3c10fd72ce24",
        "protocol": "Automatic",
        "mode": "triggered",
        "name": "ping Telegram",
        "host": "api.telegram.org",
        "timer": "20",
        "inputs": 1,
        "x": 430,
        "y": 1000,
        "wires": [
            [
                "6a052fd07e1e5a78"
            ]
        ]
    },
    {
        "id": "6a052fd07e1e5a78",
        "type": "switch",
        "z": "db3e3c10fd72ce24",
        "name": "",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "false"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 570,
        "y": 1000,
        "wires": [
            [],
            [
                "d7911cc18da7a6a5",
                "75e9c396dc233886",
                "dc4bc07536371b15"
            ]
        ]
    },
    {
        "id": "d7911cc18da7a6a5",
        "type": "change",
        "z": "db3e3c10fd72ce24",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "errorT",
                "pt": "msg",
                "to": "telegram.error",
                "tot": "msg"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "\"Telegram Polling down, Internet and Telegram OK. Error: \" & msg.errorT",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 740,
        "y": 1000,
        "wires": [
            [
                "dc4bc07536371b15"
            ]
        ]
    },
    {
        "id": "dc4bc07536371b15",
        "type": "debug",
        "z": "db3e3c10fd72ce24",
        "name": "debug 17",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 940,
        "y": 1080,
        "wires": []
    },
    {
        "id": "75e9c396dc233886",
        "type": "debug",
        "z": "db3e3c10fd72ce24",
        "name": "debug 18",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "telegram.error",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 940,
        "y": 1120,
        "wires": []
    },
    {
        "id": "81d711184cd97ddc",
        "type": "inject",
        "z": "db3e3c10fd72ce24",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "telegram",
                "v": "telegramTest",
                "vt": "global"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 260,
        "y": 1000,
        "wires": [
            [
                "aa47566793e591e0"
            ]
        ]
    }
]

if I change the inject node so msg.telegram is an object (copied from global context data using its copy data button), then the JSONata works as I expected. (new inject node)

[
    {
        "id": "81d711184cd97ddc",
        "type": "inject",
        "z": "db3e3c10fd72ce24",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "telegram",
                "v": "{\"isOnline\":false,\"error\":\"AggregateError\"}",
                "vt": "json"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 260,
        "y": 1000,
        "wires": [
            [
                "aa47566793e591e0"
            ]
        ]
    }
]

So this msg object has hidden data in it, but worse JSONata is using that hidden data whilst displays in the editor like debug node, and Context data display show different. It is like it has a hidden and not hidden value for ".error", one a string the other an object

For information the message comes from the 2nd output of a Telegram Control node from node-red-contrib-telegrambot. I think I know the cause of the error, but I would like it to report the right error in the future? This post is about hidden data, and JSONata behaving differently to NR in general

Are you able to post a complete flow that we can test to see the issue?

OK, took a bit to get it to error. But here is a cut down flow that gets the telegram node to error. In this cast the hidden data is still an object, just an empty object so the final text displays {} in place of the .error string.
the bot needs a token, so use

6766654005:AAFDSa7rwFBqZhNTiC4LL3nAWYBC9vNKN7w

it is my test bot so I will change the token when your finished.

For telegram Control node to produce an error (and thus the hidden and visible msg.payload.error, the local machine (I use Raspi 3B+) needs to have IPv6 enabled but connected to a network which has no IPv6 connection to internet. Disabling IPv6 on local machine makes the fault in telegram go away. selecting IPv6 or IPv4 in the telegram bot node doesn't help produce the node to error. There appears to be a fault in the nodejs Telegram node that node-red-contrib-Telegrambot uses, this is a separate issue.

The issue is the message 2nd output appears to included hidden data with same "key" as data in the message (msg.payload.error), JSONata uses the hidden data whilst debug nodes and set instruction in change nodes use the visible data (also Debug nodes like Debug 18 do something different).
Writing the message to Context data appears to write both visible and invisible data but context sidebar only shows the visible data

Flow is

[
    {
        "id": "81973d9ccf5fcabd",
        "type": "tab",
        "label": "Flow 1",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "aa47566793e591e0",
        "type": "ping",
        "z": "81973d9ccf5fcabd",
        "protocol": "Automatic",
        "mode": "triggered",
        "name": "ping Telegram",
        "host": "api.telegram.org",
        "timer": "20",
        "inputs": 1,
        "x": 470,
        "y": 220,
        "wires": [
            [
                "6a052fd07e1e5a78"
            ]
        ]
    },
    {
        "id": "6a052fd07e1e5a78",
        "type": "switch",
        "z": "81973d9ccf5fcabd",
        "name": "",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "false"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 610,
        "y": 220,
        "wires": [
            [],
            [
                "d7911cc18da7a6a5",
                "75e9c396dc233886",
                "dc4bc07536371b15"
            ]
        ]
    },
    {
        "id": "d7911cc18da7a6a5",
        "type": "change",
        "z": "81973d9ccf5fcabd",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "errorT",
                "pt": "msg",
                "to": "telegram.error",
                "tot": "msg",
                "dc": true
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "\"Telegram Polling down, Internet and Telegram OK. Error: \" & msg.errorT",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 780,
        "y": 220,
        "wires": [
            [
                "dc4bc07536371b15"
            ]
        ]
    },
    {
        "id": "dc4bc07536371b15",
        "type": "debug",
        "z": "81973d9ccf5fcabd",
        "name": "debug 17",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 980,
        "y": 300,
        "wires": []
    },
    {
        "id": "75e9c396dc233886",
        "type": "debug",
        "z": "81973d9ccf5fcabd",
        "name": "debug 18",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "telegram.error",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 980,
        "y": 340,
        "wires": []
    },
    {
        "id": "81d711184cd97ddc",
        "type": "inject",
        "z": "81973d9ccf5fcabd",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "telegram",
                "v": "telegramTest",
                "vt": "global"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 300,
        "y": 220,
        "wires": [
            [
                "aa47566793e591e0"
            ]
        ]
    },
    {
        "id": "ffbd13bc95170a9f",
        "type": "telegram control",
        "z": "81973d9ccf5fcabd",
        "name": "TestBot",
        "bot": "14bfeedab202a370",
        "outputs": 2,
        "checkconnection": true,
        "hostname": "",
        "interval": "10",
        "timeout": "5",
        "x": 100,
        "y": 140,
        "wires": [
            [],
            [
                "c9646e65d4c04159",
                "88214b3be526abaf"
            ]
        ]
    },
    {
        "id": "c9646e65d4c04159",
        "type": "debug",
        "z": "81973d9ccf5fcabd",
        "name": "debug 19",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 280,
        "y": 100,
        "wires": []
    },
    {
        "id": "88214b3be526abaf",
        "type": "change",
        "z": "81973d9ccf5fcabd",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "telegram",
                "pt": "msg",
                "to": "payload",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 270,
        "y": 160,
        "wires": [
            [
                "aa47566793e591e0"
            ]
        ]
    },
    {
        "id": "14bfeedab202a370",
        "type": "telegram bot",
        "botname": "WombatTest_bot",
        "usernames": "Ian Harrison",
        "chatids": "",
        "baseapiurl": "",
        "testenvironment": false,
        "updatemode": "polling",
        "addressfamily": "6",
        "pollinterval": 300,
        "usesocks": false,
        "sockshost": "",
        "socksprotocol": "socks5",
        "socksport": 6667,
        "socksusername": "anonymous",
        "sockspassword": "",
        "bothost": "",
        "botpath": "",
        "localbothost": "0.0.0.0",
        "localbotport": 8443,
        "publicbotport": 8443,
        "privatekey": "",
        "certificate": "",
        "useselfsignedcertificate": false,
        "sslterminated": false,
        "verboselogging": false
    }
]

You are in compatibility mode. I suggest using $$.errorT or just errorT

This may have nothing to do with your issue.

As to issue have you tried moving msg.payload rather than setting or try deep copy.

p.s Why are you using JSONata to create the string? The template node is better choice as less resource hungry for this type of thing.

Regarding your issue of "hidden properties", the debug output does some curation to format error objects in a particular way. However, after a little investigation, I suspect something is not quite right.

Here is a simple demo I think highlights your issue better (removing all the prerequisites by just generating errors!)

See how err and error are "empty" but errProps and errorProps demonstrate they are clearly NOT empty:

try {
    throw new Error('deliberate error')
} catch (error) {
    msg.error = error
    msg.errorProps = {
        message: error.message,
        stack: error.stack,
        cause: error.cause,
    }

    const err = new AggregateError([error],"all errors in here")
    msg.err = err
    msg.errProps = {
        message: err.message,
        stack: err.stack,
        cause: err.cause,
    }


    msg.payload = {
        info: "i am a string prop",
        number: 42,
        error: error,
        err: err,
        fakeError: {
            message: "i am a fake error",
            stack: "blah blah",
            cause: null,
        }
    }


    msg.payload.errorStringifiedParsed = JSON.parse(JSON.stringify(err))
    msg.payload.errorCloned = RED.util.cloneMessage(err);

    node.warn({ _: "the error and some props", err, message: err.message, stack: err.stack, errors: err.errors });
    node.warn({ _: "the error props spread", ...err });
    node.warn({ _: "the error Cloned", errorCloned: msg.payload.errorCloned });
    node.warn({ _: "the error stringified,parsed", errorStringifiedParsed: msg.payload.errorStringifiedParsed });
    node.warn({ _: "the fake error", fakeError: msg.payload.fakeError });

};
return msg;

I suspect this is related to how node-red is packaging data to send to the front end - likely using JSON.stringify somewhere in the chain:

err is a real error object. its properties have value.

If Node-RED is to handle/show errors better, we will likely need to test the object IS an error.

Unfortunately, Error.isError is not widely supported yet but there is an official polyfill we could adapt for Node-RED

Issue raised here: Error objects are not displayed in debug output · Issue #5068 · node-red/node-red · GitHub

Thanks for getting back, both of those suggestions made no difference. msg.errorT was introduced by me (originally the JSONata had ..." & msg.telegram.error but I added the copy to see if it would fix it. I also then added deep copy and it changed (msg.errorT became an empty object).
In the above Flow, if you change in the change node the set msg.errorT to not be a deep copy (my norm) then the output of the node changes to

{"payload":"Telegram Polling down, Internet and Telegram OK. Error: {\"code\":\"ETIMEDOUT\"}",
"_msgid":"7a9b925e7f8740d0",
"telegram":{"isOnline":false,"error":"AggregateError"},
"topic":"api.telegram.org",
"errorT":"AggregateError"}

The data appended to the string in the JSONata statement changes from empty object to the object with variable with key "code".
but msg.errorT changes back the string visible in the msg!

JSONata statements

"Telegram Polling down, Internet and Telegram OK. Error: " & $$.errorT
"Telegram Polling down, Internet and Telegram OK. Error: " & errorT
"Telegram Polling down, Internet and Telegram OK. Error: " & msg.errorT
"Telegram Polling down, Internet and Telegram OK. Error: " & msg.telegram.error

all produce the same output (deepcopy off), showing the invisible data in the payload string as above!

As demonstrated above, errors are not correctly displayed but you can simply access the properties e.g...

Thanks Steve-Mcl, I can work around this easy enough (will replace it all with a template node).

But I am raising this as inconsistent behaviour within node red which I thought is not good and should be fixed.

Just to add, I wrote the message from Telegram Control to a file context data. In the sidebar it shows its value is

{"isOnline":false,"error":"AggregateError"}

If I open the file in .context folder, (using Notepad++) it contains

{
    "Telegram": {
        "isOnline": false,
        "error": {
            "code": "ETIMEDOUT"
        }
    }
}

With this simple Flow

[
    {
        "id": "81d711184cd97ddc",
        "type": "inject",
        "z": "81973d9ccf5fcabd",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "telegram",
                "v": "#:(file)::Telegram",
                "vt": "global"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 260,
        "y": 220,
        "wires": [
            [
                "ae0f1cf2d2d5f631"
            ]
        ]
    },
    {
        "id": "1e533f4854503788",
        "type": "change",
        "z": "81973d9ccf5fcabd",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "message",
                "pt": "msg",
                "to": "telegram.error.message",
                "tot": "msg"
            },
            {
                "t": "set",
                "p": "stack",
                "pt": "msg",
                "to": "telegram.error.stack",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 460,
        "y": 300,
        "wires": [
            [
                "4f70a9383e5b8569"
            ]
        ]
    },
    {
        "id": "4f70a9383e5b8569",
        "type": "debug",
        "z": "81973d9ccf5fcabd",
        "name": "debug 2",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 660,
        "y": 300,
        "wires": []
    }
]

If I read this Context Data back the debug pane displays it as

{"_msgid":"8b233012bb14e271",
"payload":1740993795635,
"telegram":{"isOnline":false,"error":"AggregateError"},
"message":"",
"stack":"AggregateError [ETIMEDOUT]: \n    at internalConnectMultiple (node:net:1122:18)\n    at afterConnectMultiple (node:net:1689:7)"}

If this is just read from the Global Context file variable (but which is correct, Context Side Pane or the file it is stored in? So why inconsistent (file contents vse Context side pane)... but where is this other data now from. For info I have disabled the Telegram node for the readback .

I do not think this is good for consistent programing

The issue is a combination of things.

  1. Error properties are not enumerable (ref)
  2. There is a gap between the runtime and your browser meaning data needs to be serialised. Typically this is done with JSON.stringify but as demonstrated above, Error objects do not play nice.
  3. File based context also means serialisation must be performed
  4. How JSONata handles the error object is another matter entirely but i suspect similar issues.

I did raise an issue

Lets see where that leads.

In the mean time, you have a workaround :+1:

  1. How JSONata handles the error object is another matter entirely but i suspect similar issues.

Indeed - JSONata is intended to work on JSON compatible types. Error is not a JSON type, so it has to be converted - and only enumerable properties are included. That isn't a behaviour we can change.

So how do you change a message like this to JSON type, so we can visualise it and thus use the message it with confidence.

That said, the value stored in context memory is JSON when viewed in file

{
    "Telegram": {
        "isOnline": false,
        "error": {
            "code": "ETIMEDOUT"
        }
    }
}

but is shown in the Context Sidebar as

{"isOnline":false,"error":"AggregateError"}

and when you load it (using change node, set) is loaded as (into msg.telegram)

{"isOnline":false,"error":"AggregateError"}

but then a change node, set reference to msg.telegram.error.stack yeilds a string

AggregateError [ETIMEDOUT]: 
    at internalConnectMultiple (node:net:1122:18)
    at afterConnectMultiple (node:net:1689:7)

The strings "Aggregate Error" and above string from .stack are nowhere to be seen in the source data loaded from Global Context file (when opened in notepad++)! And I haven't used any JSONata statements.

since you know and expect the error to be in msg.payload.error then there are multiple ways. The simplest is to convert it to a new object with regular props.

function node

if (typeof msg.payload?.error == 'object') {
  msg.payload.error = {
    name: msg.payload.error.name,
    message: msg.payload.error.message,
    stack: msg.payload.error.stack,
    ...msg.payload.error // get extra enumerable properties that may have been added to the err object
  }
}

code is a regular property that nodejs adds to the error object (and is therefore enumerable)

That is due to how objects, when implicitly converted to a string (by whatever means) gets its own toString method called.

To clarify, an Error object has a name property. If the code that generates the error does not set the name, toString returns the error type. In this case AggregateError

Same as above (name not set, toString returns the type name) - its how JS works.

Because you directly called a getter property (in this case the getter named stack) so it returned its value.

Because it is NodeJS internal code (compiled)

If you want to dig deeper you would need to interrogate the nested errors inside the AggregateError

if (msg.payload.error?.errors)
  for (e of msg.payload.error.errors) { 
    // check e.stack & e.message props of nested errors here
  }
}

Here is a generic routine you can use to convert the error (and any nested errors)

function isError(o) {
  return o instanceof Error || (typeof o === "object" && o !== null && "message" in o && "stack" in o);
}

function toEnumerableErrorObject(o) {
  if (!isError(o)) return o;

  let errorObj = {
    message: o.message,
    stack: o.stack,
    cause: o.cause,
    name: o.name, // Preserve the error name
    ...o, // Include any other enumerable properties
  };

  // Handle AggregateError or similar custom errors with `errors` array
  if (Array.isArray(o.errors)) {
    errorObj.errors = o.errors.map(toEnumerableErrorObject);
  }

  return errorObj;
}


// usage:
if (msg.payload?.error) {
    msg.payload.error = toEnumerableErrorObject(msg.payload.error)
}
return msg

Thanks Steve for your comprehensive reply. Given in my example, the input is from a context variable file, and only has the text displayed above ("Telegram":.....), then nodered treats a key "error" as a special case and in places like debug node, context side menu and even a set operation in change node, and displays "AggregateError" instead of the error object unless you specify a key from that object (like error.code).
But cannot see where it has magiced up a value for error.stack when it is not longer in the source (the global context file). Yes it might be their if the input was direct from the Telegram Control node 2nd output, any such data was lost when written to the context variable... except error.code.
Will have a play with you JS code tomorrow in the sandpit when it get to warm outside for gardening.