Enabling "captureRejections" doesnt work
Describe the bug According to https://socket.io/docs/v4/listening-to-events/#error-handling it is possible to capture errors thrown in events by enabling captureRejections but doesn't seem to work. Is this example up to date? If this is no longer possible, is there a way to "capture" all errors thrown inside events at one location?
To Reproduce
Please fill the following code example:
Socket.IO server version: 4.5.1
Server
import { Server } from "socket.io";
require("events").captureRejections = true;
const io = new Server(3000, {});
io.on("connection", (socket) => {
console.log(`connect ${socket.id}`);
socket.on("disconnect", () => {
console.log(`disconnect ${socket.id}`);
});
socket[Symbol.for("nodejs.rejection")] = (err) => {
console.log("error catched", err);
};
});
Expected behavior Error should be catched by "socket[Symbol.for("nodejs.rejection")]"
Additional context I am trying to built a proper errorHandler and dont want to try/catch everywhere but just have a central errorHandler "captureRejections" seemed like a good solution but sadly the documentation isnt very clear on how to implement this.
Hi! I think it only works with handlers that return a promise:
io.on("connection", (socket) => {
socket.on("hello", async () => {
throw new Error("let's panic");
});
socket[Symbol.for("nodejs.rejection")] = (err) => {
console.error("error!", err);
};
});
To create a global error handler, I think the following should work:
const errorHandler = (handler) => {
return (...args) => {
try {
handler.apply(this, args);
} catch (e) {
console.error("error!", e);
}
}
}
io.on("connection", (socket) => {
socket.on("hello", errorHandler(() => {
throw new Error("let's panic");
}));
});
Thanks for the explanation! Would the global error handler also be possible to implement as middleware?
Edit 1: After testing some more your solution does work but only partly, since the event stack is a mix of async and non async functions Example stack: global (all namespaces/endpoints):
- versionChecker (non-async) (middleware)
- logContext (non-async) (middleware)
- socket logging (non-async) (middleware)
Specific namespaces (all endpoints)
- protect (async) (middleware)
- authorize (non-async) (middleware)
- event endpoint (async)
And in the event endpoint it is possible that more async/non-async functions get called, and 90% of the stack can and will return errors
I am very confused why some errors are getting catched and some aren't, I am trying to (sorta) replicate my setup which I have for my API, which has a single error handler and all async function have a asyncHandler wrapper:
import { Request, Response, NextFunction } from "express";
// So we don't have to do try catch everywhere we await a promise
export const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next);
};
I know WS are very different in how they work but is it even possible to have a errorHandler that captures all errors no matter if they are async or non-async?
I don't think this is currently not possible with a middleware, but I guess we could indeed add a way to catch sync/async errors. Open to suggestions on this!
Updated version of the error handler above, to work with async handlers:
const errorHandler = (handler) => {
const handleError = (err) => {
console.error("error!", err);
}
return (...args) => {
try {
const ret = handler.apply(this, args);
if (ret && typeof ret.catch === "function") {
ret.catch(handleError);
}
} catch (e) {
handleError(e);
}
}
}
io.on("connection", (socket) => {
socket.on("hello", errorHandler(() => {
throw new Error("let's panic");
}));
socket.on("hi", errorHandler(async () => {
throw new Error("let's panic too");
}));
});
Ah that's a bummer, guess I will just use the errorHandler wrapper with each function in the stack, your solution works flawlessly!
As for how to make this using middleware, I have no idea since I am nowhere near experienced enough to understand how the WS stack works internally.
Added in the documentation there: https://socket.io/docs/v4/listening-to-events/#error-handling
Please reopen if needed!
Sorry for opening this issue already, but is there a good way to get the socket in the errorHandler? Kinda useful if you want to send an error to the socket.
I could pass the socket to the errorHandler but this seems unnecessary since the socket gets passed in as the first argument in the handler function (Thats what I do anyways).
Added in the documentation there: https://socket.io/docs/v4/listening-to-events/#error-handling
Please reopen if needed!
Also @darrachequesne only collaborators can reopen an issue
is there a good way to get the socket in the errorHandler?
That's a good question! In that case, I think you should use a regular function (instead of an anonymous one):
const errorHandler = (handler) => {
return function (...args) {
const handleError = (err) => {
// "this" is the Socket object
console.error("error!", err);
}
try {
const ret = handler.apply(this, args);
if (ret && typeof ret.catch === "function") {
ret.catch(handleError);
}
} catch (e) {
handleError(e);
}
}
}
(manually binding this with handleError.bind(this) should work too)
Thanks for the quick response! While your solution works TS is very angry at me XD
'this' implicitly has type 'any' because it does not have a type annotation.
and
An outer value of 'this' is shadowed by this container.
How would I add typing to this? and since we are talking about TS now anyway how would I make sure that the first arg is actually "socket" wherever the errorHandler is used? Just to keep the code fool proof?
Could we make error handling on the server and Node.js with events.captureRejections set would "just work"? We are ourselves having a wrapper in e.g. Express routes to handle async functions and thrown errors. But in Express 5.0, async functions and thrown errors will be handled gracefully.
It would be nice to fix this both in socket.on() and socket.use() scenarios.
I would want this to work:
socket.on('foo', (data) => {
throw new Error('Oh noes');
});
As well as in middlewares:
io.on("connection", (socket) => {
socket.use(([event, ...args], next) => {
throw new Error("unauthorized event");
// This should still work of course as well, as an alternative to the one above:
return next(new Error("unauthorized event"));
// do not forget to call next
next();
});
});
Then I could setup:
import { captureRejectionSymbol } from 'node:events';
io.on("connection", (socket) => {
// middlewares here
socket.captureRejections = true; // or set it globally as per docs above
// Handle rejections like "throw new Error" that are not set within next()
socket[captureRejectionSymbol] = (err) => {
console.log(err);
};
// Handle next(error) calls
socket.on('error', (err) => {
console.log(err);
});
// handle specific events below here
});
Would it be possible to achieve this without having to use the error handler function erroHandler() as seen on https://socket.io/docs/v4/listening-to-events/#error-handling?
It would be very neat if this could be achieved! :)
@thernstig that would be great indeed :+1: I think we would need to switch to a custom EventEmitter implementation instead of the one provided by Node.js: https://nodejs.org/dist/latest-v19.x/docs/api/events.html#class-eventemitter
Which is totally feasible, but requires a major bump.
@darrachequesne would it just not be to use the Node.js EventEmitter, but make sure to do this on your socket class?
class MyClass extends EventEmitter {
constructor() {
super({ captureRejections: true });
}
That would solve the socket.on('error', ...) part.
For socket.use(...) you could just internally add the errorHandler as you define here: https://socket.io/docs/v4/listening-to-events/#error-handling - to handle the middleware case.
I think that would solve it all. Not sure if it would require a major version bump or not. As it seems to only add functionality to handle rejections made via throw.