-
Notifications
You must be signed in to change notification settings - Fork 7.3k
EventEmitter fails to dispatch all handlers when preceeding handler throws && domains won't prevent #5114
Comments
Thought there were some test cases covering this. I'll look into it. |
Just to be 100% clear: There has never been a guarantee that other handlers will fire if an error is thrown in an event handler. We can try/finally to clean up sockets if there's an error in the end handler, but this is unequivocally NOT a new bug, and not something we've ever intended to support. If your domain catches an error, you are now in an undefined state. This was the same for process.on('uncaughtException'). It's stated all over the docs, and in every tutorial for domains. Domains are not a "On Error Resume Next" type of thing. They're a "On Error Goto End" kind of thing. Your mission in the domain error handler is to die as gracefully as possible, as soon as possible. |
It seems this issue is abutting against other gray areas within node core. Rather than respond directly to your point,
I'll try to make a more comprehensive point which should be clearer than responding directly to your point. Error handling in node is remarkably inconsistent.This inconsistency creates a lot of problems for a lot of people, and it doesn't help that the core team hasn't entirely solved the issues themselves yet. Amongst the error-related issues are:
At the heart of all of these is consistency in how different types of errors are handled and responded to. Typically, in these sorts of discussions it's broken down into "application" and "runtime", where application is the code the developer writes and runtime is everything else. Unfortunately, this categorization lacks important distinctions, most importantly that where the error occurs is independent of the type, an error or an exception. The difference between errors and exceptions conceptually is exacerbated a bit in JavaScript, because of the Instead, the following break down (from input to output) is a little more appropriate, and will help us better discuss appropriate response strategies: Error Types
The primary goal of this breakdown is to two-fold.
Application ExceptionsThese are exceptions resulting from non-core code. These exceptions by definition have nothing to do with the current process state. Examples:
Input ExceptionsThese are exceptions that are currently thrown from node core when invalid input is provided.
Pre-IO Core ErrorsThese are unexpected errors for any number of reason. These occur in core, and may be a result of a bug or a failure to check an input boundary that resulted in a pre-IO C++ error (See #4583).
IO ErrorsThese are exceptions external to node including: seg faults, out of memory, hardware issues, or network issues.
IO ExceptionsThese are errors external to node including: file not found, etc..
Post-IO Core ErrorsSee "Pre-IO Core Errors". Post-IO Core ExceptionsThese in a way are a superset of "IO Errors" in that they are expected cases where we will be returning an error to the callback even when the bindings themselves don't return an error.
Post-IO Application ErrorsSee "Application Errors". How Things Areaka "How to deal with each type, and how to tell them apart"There are essentially 3 major issues with the current
The result is that production code requires the following hoop jumping to ensure callbacks are called for all exceptions originating from a given function call: function foo(callback) {
var d = domain.create()
try{
d.run(function() {
fs.readFile('bar', function(err, data) {
d.exit()
callback(err, data)
})
})
} catch(e) {
process.nextTick(function() {
callback(e)
})
} Why not just never use try/catch and use domains only for a quick 500 before shutting down?
There's no point binding every Buffer, EE, Stream, etc... to a domain if it's supposed to be collected very soon after on process exit. That's wasted effort and a hit to performance. That can be accomplished with a much lighter more performant implementation like I did in [email protected], which works great. Additionally, trycatch, addresses the exception issue as well. Yes, it catches all Error Response StrategiesNow we can address the following:
Which
|
RE: errors in general, what I usually do in my modules is throw if it's an error that can be detected immediately (bad/missing required input, current state does not permit the particular action, etc) and callback/'error' event for anything else. |
This is a fantastic writeup, Adam. It echoes a lot of my own frustration and confusion regarding how we're supposed to deal with exceptional cases in Node, and I agree in large part with what you have to say about domains. I think I need to spend spend some time thinking about this before I have any kind of substantial response, except to say that I think a lot of what you've done with trycatch belongs in Node proper, if only so we can handle errors consistently without having to pay a deoptimization penalty, if possible. |
Nice breakdown. This has been a heavily debated topic (as I'm sure the Are you suggesting node should start wrapped in a domain? In other words, a While I mostly understand the theoretical behind your discussion, there are
|
@CrabDude The undefined state that @isaacs is referring to is the fact that any mutable action that is interrupted by an exception can leave your application (and by extension, node core) in an inconsistent state. Consider this example: function dispatch(arrayOfCallbacks) {
if (!Array.isArray(arrayOfCallbacks)) throw new TypeError('not an array'); // (1)
for (var i = 0; i < arrayOfCallbacks.length; i++)
if (typeof arrayOfCallbacks[i] !== 'function') throw new TypeError('not a function'); // (2)
for (var i = 0; i < arrayOfCallbacks.length; i++)
arrayOfCallbacks[i](); // (3)
} Exceptions from (1) and (2) are safe because they don't mutate program state but only if the caller explicitly (and correctly!) handles the exceptions. An exception from (3) is intrinsically unsafe because there is no telling a) how many callbacks ran before one raised an exception, and b) what side effects those callbacks had. This is of course a contrived example but unless you code very, very carefully, you're going to run into a similar situation sooner or later. I don't trust myself to never make a mistake like that, let alone others. :-) Apropos the 'uncaughtException' event: it's evil, don't use it. |
+1 Leaving participation for easy follow up, very nice discussion here. |
Ben covered the "undefined state" question quite nicely in his example. I'll only add: if throwing might sometimes cause an undefined state, then the question of whether or not it will cause an undefined state is, itself, undefined. You need to know exactly why that exception was thrown, and from where, and for what reason, in order to be sure that you're handling it correctly. You list
In JavaScript, there is no fundamental difference between "Error" and "Exception", really. Whenever you throw, you are jumping all the way up the call stack to the closest frame that had a try/catch wrapped around it, or all the way to the TryCatch object that node uses in C++ land. In the best case (where undefined state is not created, like you wrap a JSON.parse() in a try/catch), it's a goto. In the worst, it's a goto that jumps somewhere you can't predict. |
@bnoordhuis @isaacs
nor did you address most of my points regarding both failures in the current error handling and potential solutions. Isaac to be clear, I understand how throwing creates an undefined state. My response was entirely a summary of all possible avenues of errors and sources of undefined state, as well as a comprehensive list of potential strategies for addressing each avoiding undefined states. The goal is to separate node (core) errors (and undefined state) from application errors (and undefined state). My proposal includes strategies for internally eliminating existing sources of undefined state, as well as separating application from core undefined states, thus requiring a restart only in issues like "Out of Memory" or core errors. Isaac, your example is correct that it could place the application in an undefined state, but from issues like #2582, most developers want that to be their concern, not node's. Equally, most developers want node to not ever enter into an undefined state to begin with. A major source of undefined state currently is application exceptions, which my proposal would eliminate entirely (their affect on core's state, not the exceptions themselves). Per the original issue, EE handlers should be wrapped in try/catch to avoid any unnecessary internal undefined state. Errors in handlers and callbacks should be passed to the active domain, and then either emitted as uncaughtException with a property like In other words, developers want to be responsible for their own state, and want node core to be responsible for itself. This is especially true in request/response-based daemons (server, SOA service, etc...) where most errors occur within the context of a request, and process can continue with a 500 on error. I laid out when errors necessarily cause an undefined state, and when they do not. Please point out which of those are wrong, so we can discuss directly which assertions are correct and which are not rather than repeating what we are in agreement about, and what the original issue already addressed, that we are in fact in an undefined state in such situations. Please provide examples for your reasons regarding which assertions are wrong, or alternatively, provide reasons and examples for why the proposed strategies would be insufficient. |
@CrabDude, @isaacs resumed it very well:
When you get a domain error your mission is to clean up resources and die. There's no reason to continue alive, it's like having a NullPointerException in java programs. If your app is wrong then fix it and run it again. You should have 1 domain error that wraps all your app and a domain per request. If the global domain receives an error you should kill the server. If a request throws an error it's ok to continue serving other requests but you should log the error with the highest priority in order to fix it asap. If I'm correct, domains were introduced to wrap user requests/responses but have never been a global "try-catch-continue". Concluding, if you get an error in the global domain, shutdown as graceful as you can. (spam) |
@gagle @isaacs stated that domains require a restart unequivocally. You agree and reiterated, yet go on to contradict yourself by suggesting:
Either errors caught by domains require a restart in all cases or they don't. I agree completely that certain Additionally, there's nothing special about /// I can throw a non-Error:
throw 'foo'
// And I can create and handle an error without throwing
callback(new Error('path required.'))
Servers do not require restarts on Invalid Input Exceptions, Application Exceptions, or Exceptions passed to callbacks. To suggest the first 2 require a restart when the latter does not merely because one throws where it should return and the other returns, is a contradiction. The undefined state is created only if the call site does not catch the Exception and allows it to propagate beyond the current stack slice, which is precisely why node core should not ever throw something @isaacs himself regularly points out as an anti-pattern. @isaacs Honestly, I find your disagreement to be entirely inconsistent with your own previous stance on the subject. I agree completely with your previous stance on the issue. Your words from try/catch/throw (was: My Humble Co-routine Proposal):
and:
and finally:
Please reconcile your previous stance with my arguments here, which I suspect we are more in agreement on than not. |
@CrabDude It's not the same to handle a request error and handle an app error. Remember that node.js is a web server, it can do other things but its mainly purpose is to serve requests. My previous comment is not contradicted. |
@gagle You need to define "request error" as this is not a technical thing. (Obviously, I understand what you mean because in my original rebuttal I acknowledge the need to return 500s) What you refer to as a "request error" is what @isaacs refers to as an error. In other words, he does not make this distinction because there is not a distinction from a node perspective. From an application perspective, the distinction is one created by the developer and relative to the application. Your contradiction is not that app errors and request erros are the same, it's that you fully agree with Isaac that domain errors unequivocally require a restart, yet suggest "requests errors" are an exclusion to that principle. |
Ok, I'm lost. I still agree with what I said in that post you linked to, but I don't see how it actually has any relevance here. Can you make the link clearer? JSON.parse() throws, which is annoying, because there's no way to reliably prevent it from receiving invalid input, so you end up having to wrap it in a try/catch. I don't think this is a good thing. I think it's a bad thing. You keep talking about "all cases" and such, and really, I'm very uncomfortable throwing away context like that. (The point of domains is to add context to your throws, after all.) Can you write like a short tl;dr summary of with some bullet points of what you're actually trying to accomplish here? This has gotten WAY off topic, and I have no idea what we're even talking about. Regarding the OP:
So, I'm closing this issue, because the requested feature is not something we can or should do. |
@isaacs
This is exactly my point. The node.js API is unintentionally identical to
|
@CrabDude Please refrain from posting any jsperf benchmarks as a reliable source. Instead write node specific benchmarks using the built in high precision timer Also I'm curious how else And can we please clarify Error and Exception? My understanding has always been Exceptions throw with the intention of being caught and Errors are unrecoverable, and will cause the program to crash. Though this can be confusing since throwing an So really I think it boils down to this: Node must always As a side, I do believe the documentation could be improved to be more specific about what input types are expected, and what the return type will be. |
@trevnorris I used jsperfs to avoid writing some myself (they're generally sufficient). I agree, we need proper performance test to verify any claims for node.
You have Error vs Exception exactly right. Though, I am trying to separate the Exception vs Error semantic from the throw mechanism due the lack of async typed catches in node. I tried to distinguish Errors and Exceptions from
The OP / wrapping handlers is a separate issue, but apart from that you have it right[1]. Most of my responses are a comprehensive defense of the reasoning for passing these Exceptions to the callbacks. Every thrown Exception removed from core is one less case requiring a restart. The closing of this issue was premature since I'm pretty sure we are in agreement, assuming @isaacs' 2 key concerns can be addressed:
We seem to disagree on the efficacy of the available solution(s). If an undefined state can be avoided, you should avoid it, yet the current EE implementation not only fails to avoid it, but exacerbates it by not calling its own internal handler. [1] I'm also arguing sync calls should return errors instead of throw, but I don't want to muddy the water now that we're on the same page. |
Cool. To simplify this topic let's agree to continue based on the following:
For me (3) seems like a simple yes. As for (2), there are a couple scenarios we must consider. For example, js developers are used to async functions returning var opts = { /* things here */ };
// oops. i'm assuming cb is a function
var cb = opts.cb;
var arg = opts.arg;
asyncFn(arg, cb); If nothing is thrown here then it'll fail silently and you'll be in a world of hurt. You also need to remember that So imho (2) is a no. |
I don't entirely follow. For Exceptions on Invalid Input: throw new TypeError('Bad arguments');
// becomes
var err = new TypeError('Bad arguments');
return process.nextTick(function() {
callback(err)
}) In your example, it currently fails silently anyways? fs.readFile('foo') // no error To me this is how it should be. If it is async, it should continue returning |
The goal shouldn't be making it easier to ignore errors. After all, the reason for the err-first callback convention is to force you to deal with errors. Sometimes a crashed program (from an unexpected thrown exception) is just the right thing to break a lazy developer's cycle of ignoring errors, and for that, That said, I completely disagree that 2) async functions should throw (from input mistakes), since then there are 2 code paths that need to be created to deal with errors from the same function, making a bad interface. If you pass a callback, it should be guaranteed to run at some time or another, and should be the only way of handling errors. |
Can you justify that statement? I was under the impression that throwing and catching exceptions is the expensive part of a try catch - ie not the try itself. If an error doesn't happen, its very inexpensive, no? |
The JIT can't do anything with the statements inside a try block because throws are computed gotos and break any assumptions that might be made about inlining. Because EEs are used everywhere, in most of the hottest code paths inside Node's JS, this would have huge knock-on effects. |
That's true but it's easy to work around by moving the try/catch into a separate function. It's an extra function call but meh, lib/events.js uses the util.is*() functions all over the place and no one complains about that. |
Sure, just answering the question. Ultimately, this kind of stuff is up to @trevnorris, and his thoughts on this stuff are pretty widely known. |
Isn't the possibility of a thrown error true for almost every single line of code? Even if you don't have a try-catch block, you don't save yourself from the possibility of throws. |
It's not the throw, it's the catch, which requires the compiler to put guards around the possibility of a throw happening in the try block. If there's no catch, all the runtime has to do is capture the call stack on throw and hand the error off to the global exception handler (which is where Node's domain-handling code gets involved, incidentally). It's the possibility of handling a throw part way up the call stack that makes try / catch problematic for the compiler. |
Is this a problem in most languages that implement exceptions? I still don't quite understand, I would think a try-catch block would only require a simple context save, which could then be handled however unperformantly once an exception is actually thrown. What is the work that needs to be done when a catch is setup? On a related note, I've heard that the performance hit comes from javascript (or perhaps v8 specifically) storing variables in the heap rather than on the stack when they're inside a try block. Do you know about the specifics of that? |
An optimizing compiler that does any kind of inlining, loop unrolling, or code removal is going to have problems with any control flow structures that can lead to unexpected branching. Sometimes static analysis can make strong guarantees about what happens on throw that can lead to efficient code being generated. When you have a dynamic language, little type information, and a JIT all in the mix, it's nearly impossible to do this on the fly, and V8 doesn't even try. Because the output of the JIT is machine code, the inline cache feeding the code generator needs to be able to reduce "context" to simple memory lookups, so it's not "just" saving a context or an activation record. (The same applies in the case of escape analysis where code in closures references the environment records outside the closure, which is what makes closures similarly tricky for V8's JIT.) As to where memory gets allocated, I don't know much beyond the fact that the inline cache and the codegen (crankshaft) in V8 do what they can to keep hot variables in registers. I don't think that has an impact on how V8 deals with exceptions. |
Gotcha alright. Thanks for the explanations! |
Additionally, domains fail to prevent this (although they do catch the error):
Lastly, this error prevents sockets from being released back into the socket pool, because node is listening on the
end
event internally whose handler never gets fired because of an application code error (see #5107).FWIW, this (both the above, and the socket release issue) works in trycatch because I wrap all handlers in a try/catch before dispatching.
If the error is occurring in core, we may not want to continue dispatching, but if it's occurring in application code, not only are we not in an undefined state, but the way EE short circuits is in fact creating an undefined state where sockets are not being released.
The text was updated successfully, but these errors were encountered: