-
Notifications
You must be signed in to change notification settings - Fork 13
RFC: adding built-in error handling support to zones #9
Comments
Landed the "official" domain postmortem today (nodejs/node@4a74fc9). It has additional information and examples. I'm in the middle of writing a post for this issue that has Zone specific usage. There are some key differences that avoid some of the pitfalls of domains, and I'm still investigating how other different features would be effected. |
Been writing up examples to make sure I understand the implications of the API, and have some questions before I can give a full assessment. How are implementers supposed to know whether to run the callback in function DoStuff() {
this._queue = [];
}
DoStuff.prototype.addone = function addone(fn) {
this._queue.push({ zone: Zone.current, callback: fn });
};
DoStuff.prototype.runall = function runall() {
while (this._queue.length > 0) {
const item = this._queue.shift();
const zone = item.zone;
const callback = item.callback;
zone.run(callback); // Or should this be runGuarded?
}
};
const ds = new DoStuff();
Zone.current.fork({ name: 'z1', handleError: (e) => console.error(e.message) })
.run(() => ds.addone(() => console.log('d1')));
Zone.current.fork({ name: 'z2' })
.runGuarded(() => ds.addone(() => console.log('d1'))); Here |
It should also be mentioned that error routing is one reason why domains failed. Users are allowed to create a private object that prevents bubbling of other's exceptions. ///// LIBRARY /////
const libZone = new Zone({
name: 'libZone',
handleError(e) {
// Log error and continue happily.
return true;
},
});
function ConnectDB() {
// Make connection to database.
}
ConnectDB.prototype.query = function query(options, callback) {
const obj = { options, callback, zone: Zone.current };
libZone.fork().run(() => makeRequest({ obj, completed }));
};
function completed(obj, data) {
const result = parseData(data); // Uh oh. Bad code here that throws.
obj.zone.run(() => obj.callback(result));
}
///// USER CODE /////
const zone = Zone.current.fork({ /* handleError 'n stuff */ });
const db = new ConnectDB();
zone.run(() => db.query({ /* options */ }, callback)); The difference between this and adding You may say that the implementer is Doing It Wrong, and I'd agree with you. But the fact remains that it will be used like this, as were domains, and may possibly leave a bad taste in the developer's mouth preventing them from using it in the future, as it happened with domains. In short, there are a lot of ways error routing can be abused by the user, and it's impossible to create an API that prevents it. Theoretically error routing is a great idea, but domains already went through that gauntlet and failed. |
@domenic just to be clear guarded functions are not applied to CallInZone? So promise reactions won't add a try/catch? |
@trevnorris @bmeck sorry for the massive delay in response. I will be a lot more responsive going forward; many apologies.
Basically: are you going to handle errors thrown by your callback with a try/catch, like a promise or observable library? Use
Whether you should use zone.run() or zone.runGuarded() here depends on how you expect The situation where run() is appropriate is if you think callers of runall expect:
Given that "DoStuff" is not a very real-world class, I can't be sure that your intention isn't the above, but I'd doubt it. More likely, runGuarded() is appropriate, as callers would not be expected to try/catch the runall() call, and callers probably prefer if execution continued without stopping on first throwing callback.
Sorry if this wasn't clear. I tried to make it so, but it probably got lost in the wall of text.
Yes, I'm aware of this concern, and that's the primary reason I wanted to ask Node people to review it. My position is that doing error routing correctly is indeed tricky and, in subtle cases, can definitely be done wrong. But I hope that simple advice will help, and coupled with fixing other issues domains have, it would still be worthwhile. The alternative is to abandon error routing in zones entirely, which seems like a missed opportunity. If we want to add zones for all the other reasons (zone-local storage, async stack trace marking, async task tracking) then ideally we should be able to also solve the "something better than uncaughtException" problem at the same time, given all the machinery already in place. The problem is the extra cost of 4 methods (run/runGuarded + wrap/wrapGuarded) instead of 2, which I think is manageable, but wanted to see what others thought.
I don't really understand this question. What does "applied to CallInZone" mean? What does it promise reactions adding a try/catch mean? |
@domenic in your example Zone.prototype.runGuarded = function (f) { // new
const oldZone = Zone.current;
setCurrentZone(this); // privileged API
try {
return f();
} catch (e) {
// actually stored in an internal slot, not a _-prefixed property
if (!this._handleError || !this._handleError(e)) {
throw e;
}
} finally {
setCurrentZone(oldZone);
}
};
Zone.prototype.wrapGuarded = function (f) {
const thisZone = this;
return function () {
return thisZone.runGuarded(() => f.apply(this, arguments));
};
}; Is enforcing the guard. Does this mean, all async queued tasks would fire Essentially the question is, does the following code get a guard? const zone1 = rootZone.fork({
handleError: handleError1
});
zone1.run(function a() {
Promise.resolve(0).then(function b() {
// am I guarded by rootZone?
throw Error();
})
}); And does the following continue to get a guard? const zone1 = rootZone.fork({
handleError: handleError1
});
zone1.runGuarded(function a() {
Promise.resolve(0).then(function b() {
// am I guarded by zone1?
throw Error();
});
}); |
setTimeout uses the equivalent of wrapGuarded (or, stores the current zone at the time it's called, then uses runGuarded in that zone).
setTimeout and Promise.prototype.then are different, so this is a different question than your above one. Promise.prototype.then knows that when the passed function is called, the promise machinery will handle the errors. So (essentially) it uses This the key difference I'm trying to get across in the post. If your library wants to catch errors and process them specially, like a promise implementation does, then use wrap/run. If you don't, then use wrapGuarded/runGuarded. |
The question of In the case of the library, only In the case of application code entering zone, the application code should handle/fail fast on exception, and so there is almost never a reason to use So the rule of thumb is |
Should be using
This is very confusing, use The fact that setTimeout magically gets guarded can lead to more swallowed errors that are unsafe since the code being swallowed may have turned into corrupted internal state. This corrupted state generally causes a throw but could then be silenced accidentally. I think a flag is needed somehow to delineate what a behavior vs system error is for this not to have the flaws of domains. I see no reason this cannot be implemented later? TC39 has this marked as ready to move to stage 1, this feature of error handling is not ready to be stage 1 as it basically is domains including the flaws of them (infectious behavior is a big one). |
No; I'm not sure where you got this "library code should be using wrap" idea from. wrapGuarded is often more appropriate.
No. Use wrapGuarded if you don't want to handle errors with try/catch (the guard will handle them).
This is not true. The behavior of setTimeout is exactly the same here as it is in the existing specs (where setTimeout calls to report the exception). That is all that wrapGuarded does: it says, invoke the thing, but if it throws an error, hand it to the current zone for exception handling. The root zone's exception handling is just HTML's "report the exception" (or Node's
Hmm, I thought I tried to make that extremely clear in the OP. It needs to be implemented now since it impacts how people will use zones:
Well, this RFC is trying to get some buy-in on the error handling feature being ready. I think you must still be misunderstanding it if you think it is basically domains, and that it has infectious behavior. Please continue asking questions until I can make it clearer. |
TBH this has worse implications in some cases than domains, if I'm understanding the spec correctly. With domains if I require within a domain it's still possible to escape. e.g. var my_module;
domain.create().run(() => my_module = require('./my_module'));
// my_module.js
if (process.domain) // wtf?
process.domain = null; Whereas I can't see a way to get around this with Zones. Nor would I be able to see that I'm not using the root Zone (meaning there'd be no // Don't want modules knowing this isn't the root zone.
const z = new Zone({
name: '(root zone)',
handleError(e) {
// Nothing should die
return true;
}
});
var my_module;
z.runGuarded(() => my_module = require('./my_module')); Now, the real problem here is that a module is only ever evaluated once. So if |
Hmm, maybe I am not understanding. You can just check if It's true that if your caller opts you in to running in a zone, you can't escape. But this is similar to any of the other ways in which your caller can mess up your environment (messing with globals or |
In the example given we have: rootZone.run(function c() {
throw new Error("boo");
});
The more I look at this the more I think the problem is the opt-out requirement for unsafe errors. By default errors tend towards unsafe. The example here shows a potentially unsafe error being swallowed. In order to opt-out (saying your code is not 100% safe), you should use I personally would think errors default to unsafe to catch, and you would need to be very specific about what errors are able to be handled. Though lets set that aside for a bit. An error is being caught, implicitly that was not marked as being safe to catch; this is mentioned in the domains post mortem. That is an infection. JS has a problem with |
This is just not true... |
|
Yeah, looks like that is a typo. It should be "The error is not caught by rootZone.run". I will correct the OP. |
Looking at the rest of your post, there still seems to be a misconception. You think the Zones allow overriding that default behavior inside a zone, so that instead of one global mutable state (the |
I am not so concerned with pathological ignoring of errors, but accidental errors that cause invalid state to be treated as safe. |
Sure. As I said, all zones do is give people a solution that is less footgun prone since it's scoped to their own code, instead of globally shared by the entire program. This seems like a good thing? |
I don't see how it is confined to their own code. Right above we see Anything on the stack can catch it still. No concurrency/memory isolation. In your example, programmers need to carefully disseminate in In the example, In synchronous situations we have the same problem using In order to escape to let unexpected errors escape, you actually need to do: const z = new Zone();
// this will be like z.runGuarded(c)
z.run(()=>setImmediate(c)); This only works because Zones are not doing stack propagation like domains. I don't really see a way to get to "unguarded" guaranteed except this. If this is done however, you will need to wrap |
@domenic Could you clarify one thing for me: const zone1 = Zone.current.fork();
zone1.run(() => net.createServer((c) => {
const zone2 = Zone.current.fork();
zone2.run(() => {
c.on('data', () => {
// Zone.current == zone1 or zone2?
});
});
}).listen(8080)); |
@bmeck will try to respond to the rest of your comment, but again you seem to misunderstand the proposal.
This is just false.
In that example |
@domenic I can see that An alternative is not to re-use the |
@domenic this may be clearer https://github.com/bmeck/zomains/blob/master/example/accidental-guard.js , backing implementation should match my understanding of Zone's behavior. |
@bmeck let me see if I can shine some light.
I think the confusion is about who handles the (SIDENOTE, There is some other zone above the child. In this example we can assume that it is the root zone, so the synchronous error would propagate to root zone which would forward to But in the above example the callback of I believe that is the correct behavior? |
@mhevery it actually does not swallow since it doesn't |
This is still under debate; the OP's semantics are that you have to return true (or maybe a truthy value?) to mark the error "handled." I had a question under "Issues to discuss" to ask whether we should use true, or rethrow. Otherwise, @mhevery's explanation is on point. I'm not sure what the linked example was meant to be illustrating, though. I'm also not sure what I certainly have read the postmortem, multiple times. I am not sure what your contention is. Is it that neither zones nor domains are perfect for resource cleanup? Yes, this is pretty clear; you can mess up with both (and with promises). A scenario like in the postmortem is complex, and cleaning up after it is complex. The "implicit behavior" section is also fairly inapplicable. There is no global mutable |
@domenic the fact that Zones are re-enabling guards whenever async thresholds are crossed, even when the original stack was not placing guards is an issue. Another issue is that, when these guards are in place there is not a concept of an unexpected error vs expected error. If the guards could be limited in some way to handle these I would not really have complaints. I'm going to change from examples to tables perhaps as another means of discussion:
[1] - cannot escape parent Scope's guard synchronously It is the automatic guarding without a full escape mechanism that seriously hoses my concept of this being a good idea. There is no means to skip the zones and propagate directly to the environment synchronously. There is a severe distrust of things that take this approach after the advice and documentation of Domains having little to no effect on people accidentally catching errors. If there was a more robust error handling mechanism / routing I would be less worried. Right now it is better than domains, but still suffers from one of the biggest troubles caused by them. On a personal note, I think guarding should not be automatic and people should need to guard at task they queue due to concurrency and shared access concerns. |
It may help to recall that this is a language feature and needs to be able to serve use cases in many environments---both Node and the browser. Being maximally inclusive is the best path for foundational language features in this way. |
I think error handling needs a closer look than merely enabling all use cases. However, I don't think discussion of domains or other existing attempts and the experience of problems they introduce is going to be relevant here if they are seen as paternalistic arguments. If the goal is to change how you can contain errors, and encourage behavior similar to uncaughtException in all possible situations, I agree that this would enable such behavior. However, my experiences would lead me to avoid this attempt when defaulting the behavior as described and not having an escape hatch. If such experience is irrelevant, I don't really have anything to say / am not sure what I was being asked to come and look at? |
@bmeck I think your experience with Domains is valuable, and we do want your input. It feels to me that we don't understand each other proposals and shortcomings. Is there any way we could have a face to face / Video meeting? |
It should be clarified for posterity that Zones and process.on('newListener', (e) => print(e, (new Error()).stack.substr(5)));
process.on('uncaughtException', () => {}); While with Zones there is no mechanism that allows observation into Zone creation. The second important distinction is that all process.removeAllListeners('uncaughtException'); The third difference is that
To quickly address the later part of this statement I'd like to point out this demonstrates avoidance of the reality of node's ecosystem. If I don't want to use a Promise-based API, that's easy enough, but Zones propagate and attach themselves to everything automatically. First I'll take the former remark about try/catch as an example: // mine.js
const Theirs = require('./theirs');
const t = new Theirs();
t.on('complete', () => {});
t.getData();
// theirs.js
const EE = require('events');
const req_caller = require('req_caller');
class Theirs extends EE {
constructor() { super() }
getData() {
try {
req_caller(info, (data) => this.emit('complete', data));
} catch (e) {
// oh no!!! something went wrong in req_caller()
}
}
};
module.exports = Theirs; Here we can see that the try/catch wrapping I understand what you're saying about each Zone error handler needing to explicitly return true, thus disregarding the potentially problematic behavior of indefinitely propagating Zones through async calls. I'd like to convey that history has shown developers aren't rigorous enough to properly clean up and needing to place a Combine that with Zone's design to scope functionality (generally a good thing) removes the ability to observe how my own errors are being handled. Going back to the comment about just not using code that uses Zones, I assume you are proposing this in order for as much code as possible to use them, and in the not so uncommon case of having 50+ dependencies/sub-dependencies it will complicate debugging when one of those thinks it's clever to do: // a.js
const z = new Zone();
let modb;
// First module to require('b'), now a owns it.
z.run(() => modb = require('b')); When the application uses // mycode.js
const modb = require('b');
const z = Zone.current.fork();
z.run(() => {
getResources(info, (data) => {
// I'd expect if modb has an exception that's unhandled by
// its own handler then it'd immediately propagate to me.
modb.processData(data, oncomplete);
});
});
// Here a.js was able to intercept my exception b/c it is in modb's
// Zone propagation chain.
function oncomplete(result) {
if (result === -1) // Something bad happened
throw new Error('ah crap!');
} The above may only be a hypothetical now, but please believe me that this will happen in the wild, and as module dependencies get deeper and as applications get more complex it may be a while before this would even be noticed, and even harder to find. An aside, the error handling mechanics proposed, down to returning true to indicate the exception was handled, was implemented in my original AsyncListener API during node v0.11 over two years ago (note: was removed in Dec '14, but added in Sep '13). When I implemented this it seemed like a great idea, but in the end the entire thing was removed because it was found to add too much complexity and made reasoning about error handling difficult (ref: nodejs/node@0d60ab3e). I have been down this road, 3 months of work to get an API very similar to this into node, so believe me when I tell you that a large part of my opposition is from having seen this fail in practice. |
@trevnorris i don't think we are on the same page. I left some comments in the code marked with
Zone's proposal deals with what happens with errors which are unhandled (which get passed to This is why I think we are not on the same page and we should step back and better understand what each side is proposing. Just an FYI, we have been using Zone's in Angular in the browser, and in Dart both in browser and server, where we have had very good experience with them. I understand that dart and browser is not node, but given that we both have had such different experiences, tells me that there is something we are overlooking about the other. I would like to get to that nugget which resulted in such different experiences. |
And as as side note. in your example:
Because your zone does not specify an error handler, then such a zone would be indistinguishable from no zone. My point is that you really have to go out of your way to mess up error handling. It's not something which happens because one forgets a wrong return value. |
Sorry, was being lazy. I'm aware. Will be more explicit in the future.
Brain puttered out near the end of last post. Let me attempt this again. Each step is numbered via // main.js
// (1) Require 'modb'.
const modb = require('modb');
// (5) Also require sutil for my own needs.
const sutil = require('sutil');
// (6) Fork off a new Zone with error handler.
const z1 = Zone.current.fork({ handleError(e) { } });
// (7) Run command in scope of z2.
z1.run(() => {
// (8) getResources is defined somewhere in main.js. Is an async
// call to retrieve something. If something goes wrong I want it
// to be caught by "z1".
getResources(info, (data) => {
// (9) Make async call to processData().
sutil.processData(data, oncomplete);
});
});
function oncomplete(result) {
// (11) This callback has been called from "z4" in "sutil.js" below.
// Because "z2", created in "modb.js", is in the .parent chain,
// the exception will propagate to, and be handled, there before
// it hits "z1".
if (result === -1) throw new Error('ah crap!');
} // modb.js
// (2) Create, don't fork, a new Zone with error handler.
const z2 = new Zone({ errorHandler() { return true; }, name: '(root zone)' });
let sutil;
// (3) Require sutil in scope of z2, so if sutil uses Zone.current.fork() the
// .parent chain propagates exceptions to modb.js.
z2.run(() => sutil = require('sutil')); // sutil.js
// (4) Fork from current zone for error cleanup/logging.
const z3 = Zone.current.fork({ errorHandler(e) { }});
module.exports.processData = function processData(data, cb) {
// (10) Fork from z3 to funnel error logging, and use this for
// data propagation.
const z4 = z3.fork();
z4.data = data;
z4.run(() => yetAnotherAsyncCall((r) => cb(r)));
}; Hopefully this is explicit enough to demonstrate the scenario I described in the previous post. In this case a seemingly unrelated module was able to inject itself info the
I understand this argument, and to some point it's valid. But in an ecosystem where applications have 50+ dependencies it only takes one to really screw everyone up. Unlike Also I think you give module authors too much credit in how vigorous they'll be cleaning up resources. If they're using domains today then they'll be using
Thanks. One failure of this type of error handling is the user's ability to properly clean up after themselves. I don't say this hypothetically. This comes from companies complaining about processes with run-away resource usage because they weren't able to handle error properly. Here's a simple resource tree scenario for incoming requests:
Now, to handle this properly any failure must propagate to all other resources to cleanly shutdown. e.g. if writing to the file fails then we need to tell requests to stop, the transform stream needs to be cleaned up and the connection then needs to be sent the error message and closed. Doing this sort of cleanup reliably, so the application doesn't hold on to gigabytes of Buffers or breaks from Anyway, hope we can talk more about this. Thanks for responding. |
You bring up an excellent point here. We should make sure that
I see you are trying to make a second root zone. Not sure that this should be allowed since there could only be one root zone. But I don't think being a child of root would change your example.
See point 1.
I think this is where we diverge. In order for this to work few things have to be true
I have a hunch that you are looking at Zones as the thing you set up for a file and all exceptions will be funneled to that Zone, where as I think of it in the context of stack frames and who called who. Where a particular function/method is declared should not have any relevance to what zone it will be in, just like a method can't assume what stack frames will be above it. Method/function does not live in a zone, it is executed in a zone Resource release and proper cleanup, can't be done with Zones in the current form. In order to do that, we need to introduce a concept of Tasks, (which we have in Zone.js implementation) but which we don't want to bring to TC39, as it would greatly complicate the discussion. The way to think about tasks is that each stack frame is associated with a zone. In addition the top most frame in a stack is associated with a Task, which is similar to Zones, but it deals with VM turns rather than Zones. Zones can enter and leave at any point, but Tasks can't. Once you have a concept of a task, then a Zone can know how many outstanding Tasks there are which could have access to a particular resource. When the number of outstanding tasks goes to zero, it should be safe to clean up. The above is not fool proof, but it is better then the ad-hoc solution that we have above. But I digress. |
Cool. So take
I'm not sure how to determine if a Zone is the root other than by name. Does this mean creating a root with the name
Async, and automatically propagates. After N async calls an exception could propagate back through that many stacks, and a number of modules, to be handled. This can cause a debugging nightmare. Here's a simpler example of how a library can intercept an application's exception: // main.js
const mlib = require('mlib');
const net = require('net');
net.createServer((c) => {
mlib.doStuff(c, (data) => {
if (data === null) // ah crap
throw new Error('goodbye world');
});
}).listen(8080);
// mlib.js
module.exports.doStuff = function doStuff(c, cb) {
const z1 = Zone.current.fork({ errorHandler() { return true } });
z1.req = { zone: Zone.current, cb };
z1.runGuarded(() => doSomeAsyncStuff(c, doneWithAsync));
};
function doneWithAsync(data) {
const req = Zone.current.req;
req.zone.run(() => req.cb(data));
} As the application author I'd expect the // mlib.js
module.exports.doStuff = function doStuff(c, cb) {
const z1 = Zone.current.fork({ errorHandler() { return true } });
z1.req = { zone: Zone.current, cb };
z1.runGuarded(() => doSomeAsyncStuff(c, doneWithAsync));
};
function doneWithAsync(data) {
const req = Zone.current.req;
(new Zone()).run(() => setImmediate(() => {
req.zone.run(() => req.cb(data));
}));
} Domains had a hard enough time getting developers to do this properly, and all libraries had to do was call |
Each Zone has a parent. The root zone is the one which has no parent, and behaves indistinguishable from the current platform behavior. Only the platform is allowed to create a root zone, everyone else must fork. I am a bit confused about your first example. You set up Why do you want In my mind the only time a library should fork or capture a zone is if it does user queue operations. (Such as implementing work queue) Could we move this discussion from hypothetical here-is-how-i-can-break-zones to concrete use cases where zones fails to perform as expected. Sorry, it is hard for me to follow, and I think the discussion would be more productive. |
@mhevery |
@mhevery code examples were given that show what were problems for domains; there is a distinct difference in:
We should setup a call sometime next week. I setup a doodle to schedule a time: |
You can tell if a zone is the root zone via Currently the spec does not prevent creating new root zones via the Zone constructor. Maybe that should be prevented, although in general giving the UA magic powers (i.e. the ability to create a zone without using the constructor) is always a bit weird to me. But we do it all over the web platform so it's probably fine. You can find the root zone by climbing the Next week is TC39 so I will not be available most days. I'll fill out the doodle but @mhevery can probably substitute. |
@domenic that only tells if the zone has a parent, zones created like https://domenic.github.io/zones/#sec-zone with a parent option of |
Right, as I said, there can currently be multiple root zones, and maybe we should disallow that. |
@domenic is there a case for multiple root zones? I would consider root zones as a realm intrinsic personally. |
Yeah, there probably isn't; it fell out naturally of the specification (and from my desire not to allow the user agent to do magic things). Happy to fix. |
@mhevery Despite my best efforts, I've failed to communicate my concerns. I'll try again, and be as clear and concise as I can be (which doesn't say much). First I'd like to address a few points:
This must be my failing. When I stated "the module I used" and "[t]here's never a good reason for a library to handle an application's exception" thought it would be apparent that
That's great you have an idea of how they should be used, but that says little of how they will be used. Unless you enforce behavior via the syntax, users will do many strange things. For example, tj/co uses generators to achieve async/await like behavior. Did anyone on the standards body consider this as a use case for generators? (this isn't rhetorical, I am curious)
Thus far nothing I've said has been simply an attempt to break the spec. It is all based on use cases I've seen in the wild. Please take my examples from this point-of-view. If I'm trying to break the spec with edge case or unrealistic scenarios I'll explicitly say so. Now for another code example that'll hopefully explain some of my reservations. The following spec assumptions were made:
Now the example code: // module.js
module.exports = getFile;
function getFile(path, callback) {
const gfz = Zone.root.fork({
name: 'module forked Zone.root',
handleError: () => true,
});
gfz.callback = callback;
gfz.zone = Zone.current;
gfz.runGuarded(() => {
require('fs').readFile(path, moduleCallback);
});
}
function moduleCallback(er, data) {
const gfz = Zone.current;
gfz.zone.run(() => {
gfz.callback(er, data);
});
}
// main.js
const getFile = require('./module');
require('net').createServer((c) => {
const nz = Zone.root.fork({ name: 'app forked Zone.root' });
nz.connection = c;
nz.run(() => {
c.on('data', connectionData);
});
}).listen(8080);
function connectionData(chunk) {
getFile(chunk.toString(), fileGotten);
}
function fileGotten(er, data) {
if (er) throw er;
const nz = Zone.current;
nz.connection.end(data);
}
Execution steps:
Scenario: I know my application is throwing, but can't find where the exception is being swallowed. Causing me angst. Challenge 1: Find where the exception is being swallowed, only being allowed to touch the code in Challenge 2: How can the author of Point: If a third-party module was swallowing my exceptions with process.on('newListener',
(n) => n === 'uncaughtException' && console.log((new Error()).stack)); If a third-party module wants complete error handling for only the duration of its execution the module can call While debugging my application or troubleshooting a module all usage Simply put, if I as the application's author don't want any module to silence my exceptions I have the APIs to make sure that happens. As far as I understand the current Zone's spec, a third-party module could swallow my exception into the abyss. Making it near impossible for me to find or debug. |
Mmmmm our meeting schedule doesn't fully align. I can say we either split it into 2 meetings, or wait another week. |
I am sorry about a delayed response. My understanding of your concern is that exceptions can be swallowed by an uncooperating library.
In the above case
Same thing using
Yes, A better way to write this would be:
Notice: that But I think don't think that is right either. I think there should only be
The reason for this change is that code should be devided into two categories. My code, and callbacks I got from someplace else (not my code). When executing code execeptions should be handled using In the above example when Also becasue we have removed the The rules are:
|
I'm missing how it's not plain to see that try catch isn't the same. Take the following example: try {
fs.writeFile(path, data, err => {
throw new Error('Nothing gonna catch me!');
});
} catch (e) { } No automated catch propagation. Can we all agree on this important difference?
Sure it's analogous, but in practical terms it's very different. The Zone will automatically propagate the try catch like behavior everywhere, through all asynchronous time.
And you don't see a problem with that? Finally, neither of my challenges were addressed. 1) How am I supposed to locate where an exception of mine is being swallowed by a module. 2) How can I synchronously execute a callback where the root Zone is the only Zone in the stack. Example for asynchronous callback execution: fs.writeFile(path, data, err => {
// Say "callback" is a user supplied callback in an
// above scope.
Zone.root.fork().run(() => setImmediate(() => callback(err)));
}); |
This is a request for comments, especially from the Node community (/cc @bmeck @trevnorris; please bring in others). Our original plan of leaving error handling out of the base zone primitive, and letting it be handled entirely by host environments or future spec extensions, is starting to show some cracks. This is mainly because it impacts the recommendations around how to "wrap" functions for zones. We would like the base zones proposal to have a strong recommendation: "to properly propagate zones while doing async work, do X."
The problem
Currently our recommendation for how to propagate zones is "use
Zone.current.wrap
", but @mhevery has shown me how that doesn't quite work in certain scenarios once error handling is introduced. In most cases of user-mode queuing, you want wrapped functions to send any errors to the relevant zone. However, in some cases, mainly when building control flow abstractions like promise libraries, you want to handle errors yourself.So, if we later introduced error handling, some fraction of the uses of
Zone.current.wrap
would be wrong.Similar problems apply to
zone.run
, since one of the important uses ofrun
for library authors is to manually do wrapping (i.e., store the callback and current zone, then dostoredZone.run(storedCallback)
, instead of storing the wrapped callback).The solution
The way to fix this is to make the two use cases explicit. Instead of
wrap
andrun
, we havewrap
,wrapGuarded
,run
, andrunGuarded
, where the non-guarded variants are used when the library explicitly wants to handle thrown exceptions: like for implementing promises or observables or similar, where you transform thrown exceptions into a different form.However, it's pretty pointless to introduce these two functions if the zones proposal doesn't actually have error handling built-in. So this takes us down the path of introducing error handling into the base zone primitive, instead of saving it for a future extension.
In other words, keeping things in a future extension is fine, unless that requires you to change your code now. If you require people to change their code now, you might as well give them the benefits (zone error handling) that you're asking them to pay for.
Details of error handling proposal
The zone fork options get a
handleError
option, which takes a thrown error object:This is pretty simple. The trick is then figuring out when and how we should route errors to the error handler. To discuss that, we need to talk about the “guarded” functions introduced above.
Details of {wrap,run}{Guarded}
Currently, we have (essentially)
We would then introduce:
How to think about these
The TL;DR is: most libraries use wrapGuarded. Most apps use run.
In a bit more detail: the user code and the library collaborate to figure out how errors are handled, with the following inputs:
Example in action
This example shows how, if you follow the TL;DR above, everything “just works”:
At the time the error is thrown, the call stack is:
c
(top)rootZone.run
b
this.storedFunction
(wrapper aroundb
to run it in zone1)sql.doStuff
e
to run it inzone2
(generated by setTimeout)Error propagation and handling then occurs like so:
rootZone.run
(run
does not handle errors at all).this.storedFunction
, which is a wrapper aroundb
to run it guarded in zone1. That sends it toerrorHandler1
.errorHandler1
doesn't return true, the error is next caught by the wrapper arounde
to run it inzone2
. So it's sent toerrorHandler2
.errorHandler2
doesn't try it, we call the error unhandled, and it goes towindow.onerror
or"uncaughtException"
as usual.Issues to discuss
Most importantly: does this sound like something that is acceptable to potential zone-using communities? We’d like to have everyone on board, and we spent a lot of time trying to get the details right here (drawing on things like the domain module postmortem from Node.js), so hopefully it’s not that bad.
Less important issues:
The text was updated successfully, but these errors were encountered: