Skip to content
This repository has been archived by the owner on Mar 25, 2018. It is now read-only.

Alternative error handling #4

Open
vkurchatkin opened this issue Mar 2, 2015 · 18 comments
Open

Alternative error handling #4

vkurchatkin opened this issue Mar 2, 2015 · 18 comments

Comments

@vkurchatkin
Copy link

With ES2015 destructuring we could get something like this:

let {err, res} = syncOperation();
// or
let [err, res] = syncOperation();

let {err, res} = await asyncOperation();
// or
let [err, res] = await asyncOperation();

We can return result as object, array or both.

@chrisdickinson
Copy link

I'm not sure the multiple-return from await is necessary – as I understand the async/await proposal, if a promise rejects it bubbles back to the caller as an exception:

function asyncOp() {
  var rej
  setTimeout(() => {
    rej(new Error('ok'))
  })
  return new Promise((resolve, reject) => {
    rej = reject
  })
}

try {
  var res = await asyncOp()
} catch(err) {
  console.log(err) // "ok"
}

cc'ing @domenic who knows more about this than I do :)

@domenic
Copy link

domenic commented Mar 2, 2015

Yes, I think @vkurchatkin is suggesting abandoning exceptions and error bubbling in favor of multi-value return (like Go, IIUC). So the promise returned would always fulfill, and would fulfill with a [err, res] tuple.

I don't like it, as I think exceptions and error bubbling add a lot to programming in general. But I know there are many who disagree with me there.

@vkurchatkin
Copy link
Author

@domenic right, inspired by Go mostly. It's basically exceptions vs errors. Exceptions are invalid input, etc. If one wants an error to bubble they can explicitly rethrow it.

@chrisdickinson
Copy link

I don't like it, as I think exceptions and error bubbling add a lot to programming in general.

I agree. They can be kind of a mess in C++, but in garbage-collected languages they're pretty nice. Manually handling every error at the callsite makes it hard to group errors naturally, easy to forget to handle them, and arduous to get the errors back to code that can usefully handle them. In short, it's exactly what we have now with node style callbacks, and a big part of what Promises would make better :)

@mikeal
Copy link
Contributor

mikeal commented Mar 2, 2015

The main issue with try/catch exception handling is that it only allows you to deal with a problem once it had turned catastrophic. The stack is gone, all the execution context is blown out, and the best you can do is work with the serialization to figure out what happened. Literally anything you can do is better than that, in that anything else you could do would allow you to work with a problem in the course of failing rather than after it has failed catastrophically.

@domenic
Copy link

domenic commented Mar 3, 2015

The main issue with try/catch exception handling is that it only allows you to deal with a problem once it had turned catastrophic. The stack is gone, all the execution context is blown out, and the best you can do is work with the serialization to figure out what happened. Literally anything you can do is better than that, in that anything else you could do would allow you to work with a problem in the course of failing rather than after it has failed catastrophically.

I don't see why any of that is true... In the cases we're discussing here, they're isomorphic.

@mikeal
Copy link
Contributor

mikeal commented Mar 3, 2015

@domenic FYI, I edited your reply because the email support in GitHub flipped out for some reason. Please doublecheck to make sure it is correct.

@domenic
Copy link

domenic commented Mar 3, 2015

Yeah all good, thanks. #windowsphoneproblems

@petkaantonov
Copy link

I agree it should be exceptions, never return values, for reasons outlined in http://yosefk.com/blog/error-codes-vs-exceptions-critical-code-vs-typical-code.html. However the language constructs when using async/await are insufficient, it will need a typed and/or predicated catch statement and a using (C#)/try-with-resources(Java)/ with(python) statement.

The main issue with try/catch exception handling is that it only allows you to deal with a problem once it had turned catastrophic.

There is no difference between error codes and exceptions WRT to this, you can try catch any call just like you can check the error code of any call. But since you are only going to propagate the error to the caller anyway, automatic propagation of exceptions is very nice and you don't need the try catch.

@yuchi
Copy link

yuchi commented Mar 23, 2015

However the language constructs when using async/await are insufficient […]

In my personal experience this is incredibly true. Even if it’s now out of the spec, _CancellationError_s are very difficult to manage in ES2015 co+yield code.

@bmeck
Copy link
Member

bmeck commented Apr 7, 2015

In my personal experience this is incredibly true. Even if it’s now out of the spec, CancellationErrors are very difficult to manage in ES2015 co+yield code.

cancellation can be managed via runners like co if they were to use generators' .return method, I use it for aborts/cancellation in https://github.com/bmeck/generator-runner. There are open issues in the spec to support cancellation via async functions, but they are not fleshed out yet. Though, I almost never use catch {} almost only finally {} now days, let the errors bubble!

@petkaantonov
Copy link

.return is not implemented anywhere

@bmeck
Copy link
Member

bmeck commented Apr 7, 2015

@petkaantonov I use regenerator which has it and firefox has it. Just because current v8 is not spec compliant does not mean we should ignore it.

xref v8 issue: https://code.google.com/p/v8/issues/detail?id=3566&q=generator%20return&colspec=ID%20Pri%20M%20Week%20ReleaseBlock%20Cr%20Status%20Owner%20Summary%20OS%20Modified

@benjamingr
Copy link
Member

Copying from nodejs/node#5020

@vkurchatkin

@benjamingr I don't think that conditional catch is a good idea in a dynamically typed language.

For what it's worth, almost every other dynamic language has it (e.g. Python) and people use it all the time. In addition, it could be a predicate - but I just wanted to give an example of how I'd deal with it.

People have been using promises with async/await, Node and Babel for years now. The whole reason the TC added async/awaic is to enable people to write code this way - otherwise we'd just stick with generators.

If I wrap all the code or write it myself I can treat rejections as programmer errors and let them bubble.

Promise rejections do not only symbolize programmer errors. This is fundamental and goes to the mental model of promises - it's what they do conceptually. Promises were born to serve as proxies for values - rejections symbolize regular exceptions and regular exceptions are not always programmer errors in the JavaScript language.

Even things that are not programmer errors you'd want to bubble - it is about ownership and control - very often you can't deal with the vast majority of errors - it is about responsibility and passing it on the same way returning passes it on. My readCSVFile method should not be in charge of what to do when the file is not there - that's why Node code is typically full of if(err) return cb(err) because the error is very often propagated manually and when it is not most of the times it is a mistake.

@YurySolovyov
Copy link

I wonder how examples from OP would evolve:

let [err, res] = await asyncOperation();
if (!err && res != null) {
  // do stuff
}

How is this better than callbacks or throwing?
Maybe I miss something, but can someone make an example with more than 1 line of code, control flow etc. ?

@bjouhier
Copy link

Copying from nodejs/node#5020

@vkurchatkin

A good way to reason about exceptions and organize EH code is to follow the contract metaphor:

  • every function that you write implements some kind of contract
  • if the contract succeeds, the function returns normally.
  • if the contract cannot be fulfilled the function throws.

If f1 calls f2, f3 and f4, and if f3 cannot fulfill its contract, then most of the time f1 will not be able to fulfill its own contract either. So the best it can do is fail (throw). Catching locally does not help (try { f3(); } catch (ex) { throw ex; } is just plain silly).

There are a few places in an application where contracts get looser. In these functions you can catch exceptions and still satisfy the contract. A typical example is a top level HTTP request dispatcher. At this level the contract becomes very loose and very generic; something like: "dispatch the request and return an HTTP response, either a success one (2xx, 3xx) or an error one (4xx, 5xx)". This is the right place to catch and return normally.

You also need to be careful about releasing resources and restoring program invariants but that's where try/finally comes to the rescue.

These classical EH patterns had become unusable in callback code because exception bubbling goes to the event loop instead of the logical caller. Async/await restores them.

@bjouhier
Copy link

@YuriSolovyov
Most of the time you would end up with:

let [err, res] = await asyncOperation();
if (err) throw err; // reject my own promise - what else can I do??
// do stuff

Instead, you should just write:

let res = await asyncOperation();
// do stuff.

There is also the fact that error codes get in the way. They force you to decompose any expression that contains async calls. With error codes, you cannot write something as simple as:

let res = syncOp(await asyncOp());

Exceptions allow you to clearly separate the EH code. Your logic is not broken / polluted by error checks, and you can review the exception handling logic separately.

@hax
Copy link

hax commented Mar 4, 2016

@petkaantonov FYI, I wrote a babel plugin to just patch generator.prototype.return for V8.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests