How to pass error object to socket.io callback - javascript

I am using callbacks with socket.io
Client code :
socket.emit('someEvent', {data:1}, function(err, result) {
console.log(err.message);
});
Server code :
socket.on('someEvent', function(data, callback) {
callback(new Error('testing error'));
});
With the above code the client side always prints out undefined. If I change the server side code to the following I can see the error message.
socket.on('someEvent', function(data, callback) {
callback({message:'testing error'});
});
I can pass my own custom objects to the client just fine, just not the error object. Any ideas?

socket.io data is serialized as JSON, which can only represent plain objects. You will need to serialize any errors into a recognizable plain-object format, or let Socket.IO's standard serialization do its thing (which will result in a plain empty object for Error instances.

I also think it's odd that Socket.IO doesn't seem to provide explicit built in support for passing Error objects in a meaningful way.
If you want to have Error objects seralized correctly in Socket.IO callbacks you can define a method like this to specify how serialization with .toJSON for Error messages should be handled:
if (!('toJSON' in Error.prototype)) {
Object.defineProperty(Error.prototype, 'toJSON', {
value: function () {
let result = {}
Object.getOwnPropertyNames(this).forEach((key) => {
if (key === 'stack') return
result[key] = this[key];
}, this)
return result
},
configurable: true,
writable: true
})
}
If you are throwing messages from the server you will want to define this on the server, and if throwing errors from a client to the server you will need to define it on the client too.
Note: In this example it strips the 'stack' property when returning errors to the client (to avoid exposing internals to a client), but this may be something worth leaving in, at least for development mode.
Update: This is a lot easier with ES6
Adapted from a blog post I wrote recently on this:
https://medium.com/#iaincollins/error-handling-in-javascript-a6172ccdf9af
I'm happy to say this easier/cleaner with ES6.
If you define your class extending Error like this:
class ValidationError extends Error {
constructor(message) {
super(message)
this.name = 'ValidationError'
this.message = message
}
toJSON() {
return {
error: {
name: this.name,
message: this.message,
stacktrace: this.stack
}
}
}
}
Instead of this being the object that gets passed back:
{ name: 'ValidationError' }
It now looks something like this:
{
error: {
name: 'ValidationError',
message: 'A validation error',
stacktrace: '…'
}
}
You can see what to expect by doing this:
console.log(JSON.stringify(new ValidationError('A validation error'))

Here's a little router function that works with plain error objects. You might want to remove the stack variable if you're shy.
socket.on('api', async (method, args, callback) {
try {
const result = await api[method](...args)
callback(null, result)
} catch (error) {
const { name, message, stack } = error
callback(error.toJSON ? error : { name, message, stack })
}
})

Related

TypeError: Cannot read properties of undefined (reading 'location') in res.redirect

I have the following function to retrieve an object from a database and extract an URL:
async redirect(id: string, redirectFunction: Function) {
if (!IdExists(id)) {
throw new Error(`ID ${id} does not exist.`);
}
const redirectLocation: string = await prisma.url.findUnique({
where: { uniqueId: id },
select: { url: true },
}).then((data) => {
return data?.url!;
});
redirectFunction('http://' + redirectLocation);
}
The function is called in the following segment of code:
app.get('/:id', async (req, res) => {
try {
redirectController.redirect(req.params.id, res.redirect);
} catch (error) {
console.error(error);
}
});
However, I get the TypeError: Cannot read properties of undefined (reading 'location'), I see that the error is related to the res.redirect method. However, when I replace it by console.log for debugging, the URL is showed properly. What may be causing this error?
This line of code:
redirectController.redirect(req.params.id, res.redirect);
Passes res.redirect (a function reference) as the second argument, but all that is passed is just the function so the res gets lost when you later try to call it. That causes the method to have a wrong this value when it executes and lots of things go wrong.
You can fix that several different ways. Once such way is with .bind():
redirectController.redirect(req.params.id, res.redirect.bind(res));
.bind() creates a small stub function that remembers the value of res so that when the stub function is called, it will be called with the right res reference and thus the this value inside the function will be correct.
Another way to solve it is to create your own little stub function:
redirectController.redirect(req.params.id, (...args) => {
res.redirect(...args);
});
When it calls your stub function, you call res.redirect() properly and pass it whatever arguments the controller called your stub function with.
As a small demonstration, you can see this effect here:
const obj = {
greeting: "Hello",
talk: function() {
if (this && this.greeting) {
console.log(`this.greeting is "${this.greeting}"`);
} else {
console.log("value of this is wrong");
}
}
}
console.log("calling as obj.talk()");
obj.talk(); // works
console.log("-------------------------");
// function we pass a method to and then call that method
function callTalk(fn) {
fn();
}
console.log("calling by passing method to another function");
callTalk(obj.talk); // doesn't work
// call it using .bind()
console.log("-------------------------");
console.log("calling using .bind()");
callTalk(obj.talk.bind(obj)); // works

async-await function: Error message not passed to the catch block [duplicate]

Reproducing the problem
I'm running into an issue when trying to pass error messages around using web sockets. I can replicate the issue I am facing using JSON.stringify to cater to a wider audience:
// node v0.10.15
> var error = new Error('simple error message');
undefined
> error
[Error: simple error message]
> Object.getOwnPropertyNames(error);
[ 'stack', 'arguments', 'type', 'message' ]
> JSON.stringify(error);
'{}'
The problem is that I end up with an empty object.
What I've tried
Browsers
I first tried leaving node.js and running it in various browsers. Chrome version 28 gives me the same result, and interestingly enough, Firefox at least makes an attempt but left out the message:
>>> JSON.stringify(error); // Firebug, Firefox 23
{"fileName":"debug eval code","lineNumber":1,"stack":"#debug eval code:1\n"}
Replacer function
I then looked at the Error.prototype. It shows that the prototype contains methods such as toString and toSource. Knowing that functions can't be stringified, I included a replacer function when calling JSON.stringify to remove all functions, but then realized that it too had some weird behavior:
var error = new Error('simple error message');
JSON.stringify(error, function(key, value) {
console.log(key === ''); // true (?)
console.log(value === error); // true (?)
});
It doesn't seem to loop over the object as it normally would, and therefore I can't check if the key is a function and ignore it.
The Question
Is there any way to stringify native Error messages with JSON.stringify? If not, why does this behavior occur?
Methods of getting around this
Stick with simple string-based error messages, or create personal error objects and don't rely on the native Error object.
Pull properties: JSON.stringify({ message: error.message, stack: error.stack })
Updates
#Ray Toal Suggested in a comment that I take a look at the property descriptors. It is clear now why it does not work:
var error = new Error('simple error message');
var propertyNames = Object.getOwnPropertyNames(error);
var descriptor;
for (var property, i = 0, len = propertyNames.length; i < len; ++i) {
property = propertyNames[i];
descriptor = Object.getOwnPropertyDescriptor(error, property);
console.log(property, descriptor);
}
Output:
stack { get: [Function],
set: [Function],
enumerable: false,
configurable: true }
arguments { value: undefined,
writable: true,
enumerable: false,
configurable: true }
type { value: undefined,
writable: true,
enumerable: false,
configurable: true }
message { value: 'simple error message',
writable: true,
enumerable: false,
configurable: true }
Key: enumerable: false.
Accepted answer provides a workaround for this problem.
JSON.stringify(err, Object.getOwnPropertyNames(err))
seems to work
[from a comment by /u/ub3rgeek on /r/javascript] and felixfbecker's comment below
You can define a Error.prototype.toJSON to retrieve a plain Object representing the Error:
if (!('toJSON' in Error.prototype))
Object.defineProperty(Error.prototype, 'toJSON', {
value: function () {
var alt = {};
Object.getOwnPropertyNames(this).forEach(function (key) {
alt[key] = this[key];
}, this);
return alt;
},
configurable: true,
writable: true
});
var error = new Error('testing');
error.detail = 'foo bar';
console.log(JSON.stringify(error));
// {"message":"testing","detail":"foo bar"}
Using Object.defineProperty() adds toJSON without it being an enumerable property itself.
Regarding modifying Error.prototype, while toJSON() may not be defined for Errors specifically, the method is still standardized for objects in general (ref: step 3). So, the risk of collisions or conflicts is minimal.
Though, to still avoid it completely, JSON.stringify()'s replacer parameter can be used instead:
function replaceErrors(key, value) {
if (value instanceof Error) {
var error = {};
Object.getOwnPropertyNames(value).forEach(function (propName) {
error[propName] = value[propName];
});
return error;
}
return value;
}
var error = new Error('testing');
error.detail = 'foo bar';
console.log(JSON.stringify(error, replaceErrors));
As no one is talking about the why part, I'm gonna answer it.
Why this JSON.stringify returns an empty object?
> JSON.stringify(error);
'{}'
Answer
From the document of JSON.stringify(),
For all the other Object instances (including Map, Set, WeakMap and WeakSet), only their enumerable properties will be serialized.
and Error object doesn't have its enumerable properties, that's why it prints an empty object.
There is a great Node.js package for that: serialize-error.
npm install serialize-error
It handles well even nested Error objects.
import {serializeError} from 'serialize-error';
const stringifiedError = serializeError(error);
Docs: https://www.npmjs.com/package/serialize-error
Modifying Jonathan's great answer to avoid monkey patching:
var stringifyError = function(err, filter, space) {
var plainObject = {};
Object.getOwnPropertyNames(err).forEach(function(key) {
plainObject[key] = err[key];
});
return JSON.stringify(plainObject, filter, space);
};
var error = new Error('testing');
error.detail = 'foo bar';
console.log(stringifyError(error, null, '\t'));
We needed to serialise an arbitrary object hierarchy, where the root or any of the nested properties in the hierarchy could be instances of Error.
Our solution was to use the replacer param of JSON.stringify(), e.g.:
function jsonFriendlyErrorReplacer(key, value) {
if (value instanceof Error) {
return {
// Pull all enumerable properties, supporting properties on custom Errors
...value,
// Explicitly pull Error's non-enumerable properties
name: value.name,
message: value.message,
stack: value.stack,
}
}
return value
}
let obj = {
error: new Error('nested error message')
}
console.log('Result WITHOUT custom replacer:', JSON.stringify(obj))
console.log('Result WITH custom replacer:', JSON.stringify(obj, jsonFriendlyErrorReplacer))
I was working on a JSON format for log appenders and ended up here trying to solve a similar problem. After a while, I realized I could just make Node do the work:
const util = require("util");
...
return JSON.stringify(obj, (name, value) => {
if (value instanceof Error) {
return util.format(value);
} else {
return value;
}
}
You can also just redefine those non-enumerable properties to be enumerable.
Object.defineProperty(Error.prototype, 'message', {
configurable: true,
enumerable: true
});
and maybe stack property too.
If using nodejs there is better reliable way by using native nodejs inspect. As well you can specify to print objects to unlimited depth.
Typescript example:
import { inspect } from "util";
const myObject = new Error("This is error");
console.log(JSON.stringify(myObject)); // Will print {}
console.log(myObject); // Will print full error object
console.log(inspect(myObject, {depth: null})); // Same output as console.log plus it works as well for objects with many nested properties.
Link to documentation, link to example usage.
And as well discussed in the topic How can I get the full object in Node.js's console.log(), rather than '[Object]'? here in stack overflow.
None of the answers above seemed to properly serialize properties which are on the prototype of Error (because getOwnPropertyNames() does not include inherited properties). I was also not able to redefine the properties like one of the answers suggested.
This is the solution I came up with - it uses lodash but you could replace lodash with generic versions of those functions.
function recursivePropertyFinder(obj){
if( obj === Object.prototype){
return {};
}else{
return _.reduce(Object.getOwnPropertyNames(obj),
function copy(result, value, key) {
if( !_.isFunction(obj[value])){
if( _.isObject(obj[value])){
result[value] = recursivePropertyFinder(obj[value]);
}else{
result[value] = obj[value];
}
}
return result;
}, recursivePropertyFinder(Object.getPrototypeOf(obj)));
}
}
Error.prototype.toJSON = function(){
return recursivePropertyFinder(this);
}
Here's the test I did in Chrome:
var myError = Error('hello');
myError.causedBy = Error('error2');
myError.causedBy.causedBy = Error('error3');
myError.causedBy.causedBy.displayed = true;
JSON.stringify(myError);
{"name":"Error","message":"hello","stack":"Error: hello\n at <anonymous>:66:15","causedBy":{"name":"Error","message":"error2","stack":"Error: error2\n at <anonymous>:67:20","causedBy":{"name":"Error","message":"error3","stack":"Error: error3\n at <anonymous>:68:29","displayed":true}}}
Just convert to a regular object
// example error
let err = new Error('I errored')
// one liner converting Error into regular object that can be stringified
err = Object.getOwnPropertyNames(err).reduce((acc, key) => { acc[key] = err[key]; return acc; }, {})
If you want to send this object from child process, worker or though the network there's no need to stringify. It will be automatically stringified and parsed like any other normal object
String constructor should be able to stringify error
try {
throw new Error("MY ERROR MSG")
} catch (e) {
String(e) // returns 'Error: MY ERROR MSG'
}
I've extended this answer: Is it not possible to stringify an Error using JSON.stringify?
serializeError.ts
export function serializeError(err: unknown) {
return JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err)))
}
And I can use it like this:
import { serializeError } from '../helpers/serializeError'; // Change to your path
try {
const res = await create(data);
return { status: 201 };
} catch (err) {
return { status: 400, error: serializeError(err) };
}
You can solve this with a one-liner( errStringified ) in plain javascript:
var error = new Error('simple error message');
var errStringified = (err => JSON.stringify(Object.getOwnPropertyNames(Object.getPrototypeOf(err)).reduce(function(accumulator, currentValue) { return accumulator[currentValue] = err[currentValue], accumulator}, {})))(error);
console.log(errStringified);
It works with DOMExceptions as well.

res in controller function fails due to being undefined

I'm new to Sails (and a jr. dev to boot) and have been working through the Sailsjs in Action book. Given some previous experience with TypeScript I wanted to experiment with the framework in that capacity. However I've hit a road block when trying to return response statuses and information via my API.
Every time I try to use the res in my controller I receive a:
TypeError: Cannot read property 'ok' of undefined. Based on the book and the documentation, I'm under the impression that would get set automatically per this example from the docs:
await User.create({name:'Finn'});
return res.ok();
and this example from the book:
signup: function(req, res) {
var options = {**grab request data**};
User.create(options).exec(function(err, createdUser){
if(err){
return res.negotiate(err);
}
return res.json(createdUser);
}
So I feel like I'm missing something pretty obvious but I'm not sure what. The project compiles just fine and I've got the documented typescript libraries installed/configured. Even matching that function there returns the same TypeError to me.
Another set of eyes would be greatly appreciated. Controller code below.
declare const sails: any;
import { boatInterface } from '../../interfaces/boat';
module.exports = {
friendlyName: 'new-boat',
description: 'create a new boat model',
inputs: {
modelName:{
required: true,
type: 'string',
},
yearBuilt:{
required: true,
type: 'number'
}
},
exits: {
success: {
description: 'New boat model was created successfully.'
},
invalid: {
responseType: 'badRequest',
description: 'The provided boat info was invalid.'
},
modelAlreadyCreated: {
statusCode: 409,
description: 'The provided boat model has already been
created.',
},
},
fn: async function (req: any, res: any){
console.log('building boat');
let boatRequest: boatInterface = {
modelName: req.modelName.toLowerCase(),
yearBuilt: req.yearBuilt
}
//confirming data has been formatted correctly
console.log(boatRequest);
let newBoat = await sails.models.boat.create(boatRequest)
.intercept('E_UNIQUE', 'modelAlreadyCreated')
.fetch();
//confirming new boat exists
console.log(newBoat);
console.log("request successful");
//res remains undefined and throws an error on attempted return
console.log(res);
return res.ok();
}
};
Here's the error with some console logs included. Thanks in advance!
building boat
{ modelName: 'kraken', yearBuilt: 1337 } <-- Request formats correctly
{ createdAt: 1566173040652,
updatedAt: 1566173040652,
id: 6,
modelName: 'kraken',
yearBuilt: 1337 } <-- new db entry is returned via fetch()
request successful
undefined <-- attempt to log the res returns undefined
(node:3738) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'ok' of undefined
at Object.<anonymous> (/Users/pitterpatter/Repos/learn/sails-learn/freeform/api/controllers/boat/new-boat.ts:67:20)
It looks like you're using actions2.
Your handler function will be given 2 parameters - inputs and exits as you've defined in the objects above it.
fn: async function (inputs: any, exits: any) {
inputs will be an object that contains parmeters given by a user that you've defined in the inputs part of your action (form data/query parameters/route parameters).
In this case it'd contain a modelName and yearBuilt, something like
{ modelName: 'lemon', yearBuilt: 2012.3 }
exits will contain a few default methods - exits.success() and exits.error(), as well as the invalid and modelAlreadyCreated which you've defined.
TLDR try this
fn: async function (inputs: any, exits: any) {
let boatRequest: boatInterface = {
modelName: inputs.modelName.toLowerCase(),
yearBuilt: inputs.yearBuilt
}
let newBoat = await sails.models.boat.create(boatRequest)
.intercept('E_UNIQUE', 'modelAlreadyCreated')
.fetch();
return exits.success(newBoat);
}
You can access the "raw express response object" deely that you're looking for by using this.res.ok(...) or this.req.something, but it's recommended you use the appropriate "exit" instead.

Argument "options" is not a valid SetOptions error on Firestore

I'm trying to copy my real time database contents to firestore. But I'm getting this error when I use the set function.
exports.copyPosts = functions.https.onRequest((req, res) => {
var i = 0;
var username;
db.ref("feeds").child("all").limitToLast(2000).once("value", function (postSnap) {
console.log(postSnap.numChildren());
postSnap.forEach(function(topic){
i = i + 1;
console.log(topic.key);
firestore.collection("topics").doc("all").collection(i+"").set({
caption: topic.child("caption").val(),
time: topic.child("time").val(),
username: topic.child("username").val(),
category: topic.child("category").val(),
pic: topic.child("pic").val()
},function(error) {
if (error) {
alert("Data could not be saved." + error);
} else {
alert("Data saved successfully.");
}
});
if(postSnap.numChildren()==i){
res.contentType('application/json');
res.status(200).send("Success");
}
});
});
});
Error Log
Error: Argument "options" is not a valid SetOptions. Input is not an
object.
at exports.(anonymous function) (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/firestore/src/validate.js:86:15)
at Object.exports.(anonymous function) [as isOptionalSetOptions] (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/firestore/src/validate.js:91:35)
at WriteBatch.set (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/firestore/src/write-batch.js:268:14)
at DocumentReference.set (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/firestore/src/reference.js:425:8)
at /user_code/index.js:2172:47
at /user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/cjs/src/api/DataSnapshot.js:126:20
at LLRBNode.inorderTraversal (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/cjs/src/core/util/SortedMap.js:170:13)
at LLRBNode.inorderTraversal (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/cjs/src/core/util/SortedMap.js:169:27)
at LLRBNode.inorderTraversal (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/cjs/src/core/util/SortedMap.js:169:27)
at LLRBNode.inorderTraversal (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/cjs/src/core/util/SortedMap.js:169:27)
The API documentation for the set() method states that it takes two arguments:
data: A map of the fields and values for the document.
options: (Optional) An object to configure the set behavior.
You're passing it two arguments, an object, and a function. It appears that you expect the function to be called in the event of an error, but that's not how the documentation is saying that it works. The error you're getting the API seeing that you're not passing a valid options object at the optional second parameter.
If you're interested in the result of the set() operation, pay attention to the promise that it returns. The convention is that a promise will be rejected in the case of an error. If you're going to be writing Cloud Functions code, you definitely need to be familiar with how promises work.

how i return object contain function in meteorjs?

i currently using meteorjs 0.9.2
i want to return an object from a server method to client method call
here in that server returning object contain a function as value, i think its possible to do with meteorjs EJSON
server method return object given below
return EJSON.stringify({
plotOptions: {
series: {
stacking: 'normal',
point: {
events: {
click: function() {
alert('ok');
}
}
}
}
},
});
client method receiving given below
Meteor.call("highcharts", Session.get("method"), Session.get("taskId"), function(error, object) {
$("#highcharts #loading").hide();
if(error) throwError(error.reason);
else $("#highcharts").highcharts(JSON.parse(object));
console.log(EJSON.parse(object));
});
but in browser console log i cant get that object element value as function, it show an object given below
{"plotOptions":{"series":{"stacking":"normal","point":{"events":{}}}}}
how i pass a object contain function as return ?
The correct way to solve such problem is to have all your functions of interest defined on the client side, and then choosing the appropriate function based on the EJSONable value you pass. If this is a common pattern in your app, you can for example create a dictionary of possible actions:
Actions = {};
Actions.alertOk = function() {
alert('ok');
};
Actions.confirm = function(message) {
if(confirm(message)) alert('ok');
};
...
Then in your return statement pass the action name:
return {
...
action: {
name: 'confirm',
arguments: [
'Do you want an OK alert?',
],
}
};
And then call the requested action when needed:
Actions[action.name].apply(this, action.arguments);
You could use toString on the server and eval on the client.
//server
var str = (function click() {
alert('ok');
}).toString();
//client
eval(str)();
Just make sure you understand the implications of using eval.

Categories

Resources