-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Specifying unhandledrejection
behavior
#16
Comments
From nodejs/node#46262 (comment)
To elaborate slightly here: In our own weird environment, we actually just store the context where the Promise is created, since that's enough in practice for the limited usage pattern. We use a home-grown AsyncContext analogue to track what is the "current application" out of multiple things that are all running in the same V8 isolate, as described in this talk. |
At Bloomberg, for our use-case, which is to track the logical one-of-many application responsible for causing an unhandled promise rejection, the At all places where we can have async continuations (Promise, setTimeout, etc) we restore the context (simply which application is running) that was active when we ran the initial Our promise rejection handling is done in C++ and we pull out the application and terminate it. If we were to allow applications to directly call other applications and pass promises between them, I’m not sure either of the choices given here would be correct. A promise is usually unhandled because someone forgot to handle the possibility of it rejecting. If we imagine application 1 created a promise, had an API to return it to application 2, and then at some point application 1 rejected it, it really is the fault of application 2 for not registering a rejection handler, so application 2 is who we should penalize and terminate. In all likelihood application 2 would have resolution handlers though, which would create additional promises, meaning |
I think this means that you just need the The interesting case is only where the |
Yeah, just the |
During today's meeting, there were no objections to using call the reject's call time context for However, it was brought up that this would diverge from node's implementation, which uses initialization time ( It's actually possible achieve the same performance with both call time and init time. Call time is simpler to specify (and I think implement), but both should be workable. If we believe that init time is the better behavior, we can do that. @legendecas and I will attend a node working group to discuss. |
A bit off topic maybe as not only related to unhandled rejection. |
I don't think the difference is observable in userland (except in If we decide to use |
The difference in the normal case is observable if the promise creation happens at a place where context A is active and |
const { AsyncLocalStorage } = require('node:async_hooks');
const als = new AsyncLocalStorage();
const p = als.run('A', () => Promise.resolve());
als.run('B', () => {
const p2 = p.then(() => {
// Logs B
console.log(als.getStore());
});
}); ALS logs |
Ah ok. So it seems what nodejs/diagnostics#389 describes is actually no longer true. |
Yah, it seems going back to at least node 12 this is the case. |
I don't think either of const variable = new AsyncContext.Variable();
// All should print REG
variable.run("REG", () => {
queueMictrotask(() => console.log(variable.get()));
});
variable.run("REG", () => {
promise.then(() => console.log(variable.get()));
});
variable.run("REG", () => {
setTimeout(() => console.log(variable.get()), 1000);
});
// Doesn't use registration time, what?
variable.run("REG", () => {
eventTarget.addEventListener("some-event", () => {
console.log(variable.get());
});
}); Like I see the use for What would make more sense is if globalThis.addEventListener("unhandledrejection", (event) => {
event.rejectionSnapshot.run(() => {
// Find out the state of async variables at reject time
});
}); |
The difference I see between const requestCtx = new AsyncContext.Variable();
export default onFetch(req: Request): Response {
const id = new Uuid();
return requestCtx.run(id, handler, req);
}
function handler() {
// 2 unhandledrejection will be registered before any rejection events are fired.
addEventListener('unhandledrejection', ({ reason }) => {
const id = requestCtx.get();
// This will error out!
assert.equal(id, reason);
});
Promise.reject(requestCtx.get());
}
// 2 requests fired in parallel
onFetch(new Request());
onFetch(new Request()); The
@Qard suggested something like this in our recent meeting. |
Generally what async_hooks (and therefore also AsyncLocalStorage) has done is to follow innermost logical path as it ensures data availability even within internals logically following from the point where it was stored. When following that path a In the case of promises, it would make the most sense that it follows the point of the resolve/reject call. Currently Node.js actually follows the init which mostly works fine but it does have the quirk of hiding behaviour internal to the promise executor and behaving strangely if you extract the resolve/reject functions and use them elsewhere. If it's bound to the call location then the other two points of init or continuation can be bound around that part of the graph with the The distinction between init and call location is a bit confusing and ambiguous with raw promises, but with async/await it becomes somewhat clearer as the context branches from the current function into the execution of a promise-returning function and then that graph merges back at the point of resolving. In doing so it allows new storage states to be set during an await and accessible after that await resolves. Now in some cases it may be desirable to restore the state from before the await which again can be done with something like The core thinking that is required to make this maximally useful I believe is to approach it from the perspective of tracing the lowest-level graph and providing the tools to conditionally draw edges around things when certain internals are not relevant or should not propagate data through. This behaviour of grafting new edges onto the graph adds a lot of flexibility but also different users can have different opinions of how their store should propagate around things. This is why I had previously suggested having not just a static Side note: I'm planning on eventually proposing some subset of diagnostics_channel to TC39 which includes a mechanism for using a channel to communicate an optional storage binding decision through the use of channel.bindStore(...) and channel.runStores(...). Those parts of diagnostics_channel are how Node.js currently aims to support APMs controlling where and when to manipulate their own store contexts. If we can have AsyncContext with instance-scoped bind I can wire it up to that and cover any cases APMs need, so long as the default is to follow the inner-most logical execution path to propagate through. |
Re: nodejs/node#46262
What do we imagine is the correct context when an
unhandledrejection
is triggered? Given:Our options include:
reg
, which is the context at the time of theunhandledrejection
's handler's registrationinit
, which is the context at the time of the promise's creationcall
, which is the context at the time of the promise's rejectionundefined
, which is the context at the timeunhandledrejection
handler is calledAlso relevant, the call-time choice allows does not prevent you from using init-time behavior, you just need to
wrap
yourreject
:The text was updated successfully, but these errors were encountered: