Add Recovery Method - #6
groundwater wants to merge 0 commit into
Conversation
7278688 to
9409655
Compare
|
LGTM! Here's an initial spike. |
|
I'm a fan of this approach. It offers a pretty decent way to handle errors in a promise chain . As opposed to fully rejecting a promise and making the assumption no other function in the chain can continue, it allows you to pass the error object down the promise chain so that further functions can determine whether or not they can recover. Then the error can be handled sanely outside the chain. |
|
A question and a comment @chrisdickinson
|
|
This is a nice idea but I think we could solve this without adding an actual argument and conflating the entire core API. I remind you that promises are designed to be subclassed in ES2015. We can extend promises (in a standard compliant way) with a This means you can't substitute native promises with bluebird - but I don't think that was a goal anyway. |
|
Hey @benjamingr do you have an example API you're thinking of? I'm also not sure what you mean by "conflating the core API". It sounds like you're requesting something like let file = await fs.getFileAsync('dne').recover('EISDIR', () => null)
if (file === null) {
return
} |
|
I'm on mobile, so sorry for being a little unclear and not posting code - I hate it when other people do that. I like the object API you have. Basically, recover would take an object and behave exactly like in this proposal. The only difference is that it'd be chained off rather than a function argument. By conflating I mean that as an extra argument each core method would have to be separately aware of it - in addition users that want to use it in their code would have to roll it manually (not a lot of code to write but no code is better than code). It could handle common Node errors like enoent and such (again, exactly like in the parameter variant). We'd be amendable to adding it to bluebird if core adopts it. I think it's really ergonomic. |
let file = await fs.getFileAsync('dne').recover({
EISDIR(err) {}
ENOENT(err) { ... }
}); |
|
@benjamingr thank you for explaining. To clarify, do you see the following as recovering or failing: fs.getFileAsync('dne').then(() => fs.getFileAsync('dne')).recover({ ENOENT() {...} })Specifically does the "error" fall through to the final recover handler? |
Yep — that is the case with the reference implementation. The only code that can reject the promise is located here — which is to say, only the wrapped callback API (the
Node should generate all operational errors with |
|
I'd caution against the
It looks like |
|
I'm just getting caught up on the many thread(s) again, so forgive me if I've missed something. I know there's been a lot of cross posting between this thread and some of the post-mortem threads. I'll have to think about this a bit more, but on first impression - kinda liking what I see here. I might also suggest a potential catch all for all operational errors. I'd say it's the rare case where I make the differentiation between error types when @chrisdickinson I think you may have touched on this, but if my understanding is correct, this solution would be specific to the promisified core APIs? Unless everyone is using verror or restify-errors to create error objects, userland created Error objects won't consistently have a |
|
@DonutEspresso Sorry for the delay in response! Would you accept
This pattern, as proposed, is specific to the Node API. I'm not sure we can solve the behavior of existing packages immediately — my suspicion, though, is that if this pattern is introduced by core it will be emulated by the various shims, and more broadly emulated by the ecosystem. I am curious: as an alternative, if we added |
Errors are thrown synchronously inside promise chains all the time. That wouldn't work and would break the ecosystem. |
Ah! I'm not talking about new Promise((resolve, reject) => {
throw new Error() // or reject(new Error())
})Which (should be) far less common. |
|
(This could be paired with patching the |
Why don't we want to be in it? Promises were built with subclassing ability exactly for us to be able to do it. It's a lot cleaner than patching promises, it allows reuse and so on. It lets I'm not sure why it would be confusing to users, you can still globally swap out
Why? It would use
The stack would be exactly the same wouldn't it? Instead of a callback inside we take a callback outside. For what it's worth, native promises are a little silly in that they always use the microtask queue. Bluebird for instance avoids it if it can prove the promise doesn't resolve synchronously anyway and avoids the extra queuing.
Again, subclassing is not augmenting - it is extending. I'm not suggesting we alter the
Yes, it does. This allows chaining and nesting which the current proposal does not. Instead of a "magical" parameter we just extend the promise interface (in a spec compliant subclass way) and add a Here is a fully concrete example you can run on your own computer, the actual example is at the bottom: "use strict";
var fs = require("fs");
class CorePromise extends Promise {
recover(o) {
return this.catch(e => {
if(o.hasOwnProperty(e.code) && typeof o[e.code] === "function") {
return o[e.code].call(undefined, e);
}
return CorePromise.reject(e); // or `throw e`.
});
}
}
function readFilePromise(filename) {
// a terrible way to promisify, just for the example
return new CorePromise((res, rej) => fs.readFile(filename, (err, data) => {
if(err) return rej(err);
else return res(data);
}));
}
readFilePromise("nonExisting").recover({
ENOENT(e) { console.error("Not Found :(", e.path) }
});
readFilePromise("actuallyADirectory").recover({
// not caught, propagates to an unhandledRejection
ENOENT(e) { console.error("Not Found :(", e.path) }
});
// this handler gets called for the directory since ENOENT does not match it.
process.on("unhandledRejection", (e) => console.error("Unhandled Rejection", e)); |
|
We've moved the conversation here. |
|
@benjamingr thank you for explaining your idea further. Unfortunately I think your example is a counter-example, in that it uses I think the central issue is that, by passing the recovery object as a parameter, the API can immediately decide if an error is recoverable or not. I am unclear with the fall-through behavior how an API can decide if a handler is in play or not. The recovery information must be available directly to the API, meaning the API must know if the handler exists or not. It would be worth finding the boundaries of this solution regardless, because I bet it will come up again. Having a clear example explaining the pros/cons is probably worth the time (even if it doesn't make it into core). |
Why? Just having something that can be described with a Remember, the actual promise code executes after the handler has been attached since promise handlers always execute asynchronously. This would be the same with a |
Having a This isn't to say a |
|
I was under the impression that if the object is not present or it is present but not handling the error than the rejection would propagate normally outside. How would it help post-mortem? |
|
@benjamingr in the above example, attaching a recovery object will run The only working promise "compromise" so far with post-mortem is that you cannot use If you prefer |
|
@groundwater oh, I think I understand where the miscommunication is stemming from. Just because I can implement it with As far as post-mortem is concerned this can be implemented without a Note though, that there is an implicit try/catch around things awaited in async functions and terminating on rejections that might be handled. So those assumptions all fail in async/await code. We would need V8 to expose whether or not the enclosing async function is running the code contains a try/catch block (what @spion mentioned). |
|
@benjamingr looks like @chrisdickinson and I remain unconvinced this approach is the right investment. I don't want to block you from exploring it, but I don't see enough evidence to alter this PR significantly. I do think we should add a link to your comment into the document for anyone wishing to explore this type of API. If you'd like to PR a new section adding this approach, I would happily except it instead of just linking to your comment. |
|
@groundwater a last-parameter-object is currently blocked on the possibility cancellation tokens will be introduced in the future. I'm not suggesting we add |
|
Cancellation tokens? To the Node API or to Promises? If the latter, then it shouldn't affect the recovery object, which never touches the promise API. |
|
If cancellation tokens will be added they'll be passed as a last parameter to the function. So - the former. This is some nice reading. |
|
@benjamingr Interesting — I'll give that thread a more thorough read this evening. First impression is: this means that cancelation tokens are passed to async-branded functions, not to promises themselves? That seems like it might be a bit of an overreach on the part of the spec, since it means that the spec would start dictating how Node presented its API. |
3a33275 to
d011c1b
Compare
@chrisdickinson @zkat
Your comment is a pretty good start. Figure we can go from here.