This is an archived snapshot of W3C's public bugzilla bug tracker, decommissioned in April 2019. Please see the home page for more details.

Bug 26517 - Methods that return promises are unable to throw exceptions
Summary: Methods that return promises are unable to throw exceptions
Status: RESOLVED WONTFIX
Alias: None
Product: WebAppsWG
Classification: Unclassified
Component: WebIDL (show other bugs)
Version: unspecified
Hardware: PC All
: P2 normal
Target Milestone: ---
Assignee: Cameron McCormack
QA Contact: public-webapps-bugzilla
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2014-08-04 18:32 UTC by Ian 'Hixie' Hickson
Modified: 2014-10-09 11:16 UTC (History)
16 users (show)

See Also:


Attachments

Description Ian 'Hixie' Hickson 2014-08-04 18:32:04 UTC
http://heycam.github.io/webidl/#es-operations

The spec for operations says:

# If O has a return type that is a promise type, then:
#  1. Let reject be the initial value of %Promise%.reject.
#  2. Return the result of calling reject with %Promise% as the this
#     object and the exception as the single argument value.

This is bad practice, IMHO, as discussed in detail here:
   https://github.com/domenic/promises-unwrapping/issues/24

If I want my API to reject the promise, then I'll reject the promise. If I want it to throw an exception, then I should be able to throw an exception.

In particular, if an API is called with bad arguments, then the exception should throw immediately, just like if it's name was typoed. The behaviour of:

   foo.bar(-1); // only accepts positive numbers

...should be the same as the behaviour of:

   foo.baz(1); // there's no "baz" method, it's called "bar"
Comment 1 Boris Zbarsky 2014-08-04 18:44:59 UTC
> This is bad practice, IMHO

Others disagree, right?

Note that the current webidl language has shipping implementations and specifications depending on it, by the way.
Comment 2 Jonas Sicking (Not reading bugmail) 2014-08-04 21:20:27 UTC
Yes, there seems to be different opinions of what makes good API design here.

In particular, people have expressed the opinion that having functions that can fail both synchronously and asynchronously means that developers that want to do error handling now have to "catch" errors in two different ways. And that this is a bad thing. Personally I've found this argument fairly convincing.

The strongest counter argument to me has been that some errors are due to runtime behavior and so makes a lot of sense to want to handle during runtime code. For example IO errors or users answering "no" in a security dialog.

Other errors are due to plain old bugs in the code which can make a certain line of code fail every single time. Such as forgetting to include all arguments, or using the wrong type for an argument. It's good if we can surface such errors as quickly as possible, and in a way that makes debugging as easy as possible.

However as long as developer tools gain knowledge about promises and can surface rejections as well as they traditionally have surfaced exceptions, then it seems like that takes care of this concern.
Comment 3 Tab Atkins Jr. 2014-08-04 22:23:50 UTC
(In reply to Jonas Sicking from comment #2)
> Yes, there seems to be different opinions of what makes good API design here.
> 
> In particular, people have expressed the opinion that having functions that
> can fail both synchronously and asynchronously means that developers that
> want to do error handling now have to "catch" errors in two different ways.
> And that this is a bad thing. Personally I've found this argument fairly
> convincing.
> 
> The strongest counter argument to me has been that some errors are due to
> runtime behavior and so makes a lot of sense to want to handle during
> runtime code. For example IO errors or users answering "no" in a security
> dialog.
>
> Other errors are due to plain old bugs in the code which can make a certain
> line of code fail every single time. Such as forgetting to include all
> arguments, or using the wrong type for an argument. It's good if we can
> surface such errors as quickly as possible, and in a way that makes
> debugging as easy as possible.

Countering this, I've written algos that could detect *some* of these types of errors syncly, but others had to wait for async stuff to come in.  It's not obvious a priori which types of mistakes fall into which category, as it's intimately related to the details of the algorithm.  Having some TypeErrors and similar things happen as thrown exceptions and others happen as rejected promises is just all kinds of confusing.
Comment 4 Mounir Lamouri 2014-08-05 07:17:10 UTC
(In reply to Jonas Sicking from comment #2)
> Yes, there seems to be different opinions of what makes good API design here.
> 
> In particular, people have expressed the opinion that having functions that
> can fail both synchronously and asynchronously means that developers that
> want to do error handling now have to "catch" errors in two different ways.
> And that this is a bad thing. Personally I've found this argument fairly
> convincing.
> 
> The strongest counter argument to me has been that some errors are due to
> runtime behavior and so makes a lot of sense to want to handle during
> runtime code. For example IO errors or users answering "no" in a security
> dialog.
> 
> Other errors are due to plain old bugs in the code which can make a certain
> line of code fail every single time. Such as forgetting to include all
> arguments, or using the wrong type for an argument. It's good if we can
> surface such errors as quickly as possible, and in a way that makes
> debugging as easy as possible.
> 
> However as long as developer tools gain knowledge about promises and can
> surface rejections as well as they traditionally have surfaced exceptions,
> then it seems like that takes care of this concern.

The only errors that can be reliably sync are the errors related to the received input. If you expect a positive integer and get a negative one, realising that should definitely be a synchronous operation. However, IO errors or permissions errors might be sync or async as Tab pointed.

I agree with the others that having all errors rejecting the promise would make everyone's life easier. Having to handle sync and async errors would make the code harder to read and more complicated because there would be two code paths.
Comment 5 Olli Pettay 2014-08-05 09:06:30 UTC
The platform effectively has this behavior in non-Promise cases, like
XHR.send().
Success is reported via load event, some errors via error via and in some
cases the method throws.

And, throwing early when the state of the object is wrong sounds right to me.
Requiring to use reject for that is overkill.
Comment 6 Marcos Caceres 2014-08-05 14:55:35 UTC
(In reply to Olli Pettay from comment #5)
> The platform effectively has this behavior in non-Promise cases, like
> XHR.send().
> Success is reported via load event, some errors via error via and in some
> cases the method throws.
> 
> And, throwing early when the state of the object is wrong sounds right to me.
> Requiring to use reject for that is overkill.

If you read over [1], you will see plenty of justification as to why having two separate modes of error handling is a bad idea. Basically this is a sad pattern:

```
try{
   var promise = foo().then(whatever).catch( asyncErrorHandler )
}catch(e){
    syncErrorHandler(e)
}
window.onerror = moarErrorHandling
```

I feel strongly that what we currently have specified in WebIDL is most sane. I did originally hold Hixie's position (that it's pretty easy to do all the necessary type checks through WebIDL before crossing the async boundary - and that it's traditionally not how the API's of the web work), but have been convinced we should just let the promise machinery handle the rejection. Primarily, unhandled errors are showing up in the error console of browsers, so it's trivial enough for web devs to catch and fix them during development.

[1] https://github.com/domenic/promises-unwrapping/issues/24
Comment 7 Ian 'Hixie' Hickson 2014-08-05 18:25:17 UTC
If you read over [1], you will see plenty of justification as to why having only one mode of error handling is a bad idea too. Let's not pretend that there's any kind of consensus on this issue, either. Plenty of people commented on that issue in agreement with the idea that we shouldn't turn type checks into promise rejections.

All this bug is asking for is the ability to write IDL without having to have hacks to sidestep this exception-wrapping nonsense. I would much rather be able to write:

   Promise<Foo> bar();

...than have to write:

   interface Whatever { };
   typedef (Promise<Foo> or Whatever) PromiseFoo;

   PromiseFoo bar();

...which is what I'm currently forced to do to describe APIs that throw in certain cases and return promises when they don't throw.
Comment 8 Marcos Caceres 2014-08-05 18:48:21 UTC
(In reply to Ian 'Hixie' Hickson from comment #7)
> If you read over [1], you will see plenty of justification as to why having
> only one mode of error handling is a bad idea too.
> Let's not pretend that
> there's any kind of consensus on this issue, either. Plenty of people
> commented on that issue in agreement with the idea that we shouldn't turn
> type checks into promise rejections.

True. Strong arguments are made on both sides - just saying that I personally landed on a particular side. 

I'm asking that those that are participating in this discussion consider the arguments very carefully because what you are asking to change here has massive implications (for both shipping and all future APIs that rely on promises). 

> All this bug is asking for is the ability to write IDL without having to
> have hacks to sidestep this exception-wrapping nonsense.

No, that's not all this bug is asking for: it has the potential to have significant implications for implementers, spec editors, and authors. If we get some in-between option, like "[throwOnInvalidInput]" or whatever, then some APIs might use this and others might not... then, as an author, you always need to check the API contract (so to wrap in try/catch); as a spec Editor, you need to decide if you want to use that and have a good justification as to why (and we risk big f'ups, like with everyone copy/pasting [NoInterfaceObject] without people understanding what it does); and as an implementer, you need to clearly separate the async and sync checks - where right now promises just deal with it (even if it's "exception-wrapping nonsense").  

> I would much rather
> be able to write:
> 
>    Promise<Foo> bar();
> 
> ...than have to write:
> 
>    interface Whatever { };
>    typedef (Promise<Foo> or Whatever) PromiseFoo;
> 
>    PromiseFoo bar();
> 
> ...which is what I'm currently forced to do to describe APIs that throw in
> certain cases and return promises when they don't throw.

IIUC, what we are trying to agree on is what model we use for the Web (what the web used from the beginning of time or this new model that is currently in WebIDL - or something in between, where there is a very good reason to not have the promise handle the error through rejection). If you've managed to cleverly hack around it using a typedef doesn't really address the core problem - as whatever API you put that on may be rejected by implementers unless we get agreement on the idiom to use.
Comment 9 Tab Atkins Jr. 2014-08-05 20:13:06 UTC
(In reply to Marcos Caceres from comment #8)
> (In reply to Ian 'Hixie' Hickson from comment #7)
> > I would much rather
> > be able to write:
> > 
> >    Promise<Foo> bar();
> > 
> > ...than have to write:
> > 
> >    interface Whatever { };
> >    typedef (Promise<Foo> or Whatever) PromiseFoo;
> > 
> >    PromiseFoo bar();
> > 
> > ...which is what I'm currently forced to do to describe APIs that throw in
> > certain cases and return promises when they don't throw.
> 
> IIUC, what we are trying to agree on is what model we use for the Web (what
> the web used from the beginning of time or this new model that is currently
> in WebIDL - or something in between, where there is a very good reason to
> not have the promise handle the error through rejection). If you've managed
> to cleverly hack around it using a typedef doesn't really address the core
> problem - as whatever API you put that on may be rejected by implementers
> unless we get agreement on the idiom to use.

Indeed. I'll strongly recommend not implementing such a pattern to any implementors of a feature you write that for; it's clumsy and won't match the rest of the platform.
Comment 10 Jonas Sicking (Not reading bugmail) 2014-08-05 22:24:31 UTC
>    interface Whatever { };
>    typedef (Promise<Foo> or Whatever) PromiseFoo;
> 
>    PromiseFoo bar();

As an implementer, I certainly have no interest in implementing something like this. We should come to an agreement of what best practice is, encode that into WebIDL, and then stick to it.

If every spec editor spec their own personal favorite API patterns then we'll get neither consistency nor quality in the web platform.
Comment 11 Jonas Sicking (Not reading bugmail) 2014-08-06 01:15:17 UTC
Also keep in mind that a lot of ES infrastructure is going to be created around Promises which is likely going to be optimized for functions which return rejected promises rather than throwing.
Comment 12 Boris Zbarsky 2014-08-06 03:56:56 UTC
>   typedef (Promise<Foo> or Whatever) PromiseFoo;
>   PromiseFoo bar();

I think Web IDL should disallow this, just like it disallows having overloads some of which return a Promise and some of which do not; there are no sane non-hacky use cases I can think of for it.
Comment 13 Ian 'Hixie' Hickson 2014-08-14 20:32:21 UTC
> If we get some in-between option, like "[throwOnInvalidInput]" or whatever,
> then some APIs might use this and others might not... then, as an author, you
> always need to check the API contract (so to wrap in try/catch)

No, you don't. Nobody puts every method call in a try-catch today, just like nobody is going to be checking for a rejection on a promise for this kind of bug.

As a developer you never need to look at what the API will do when you use it incorrectly. You only need to use it correctly.

When you use it incorrectly, you should get an exception, the same way as when you call the wrong API.
Comment 14 Dimitri Glazkov 2014-08-14 21:01:28 UTC
(In reply to Ian 'Hixie' Hickson from comment #13)
> > If we get some in-between option, like "[throwOnInvalidInput]" or whatever,
> > then some APIs might use this and others might not... then, as an author, you
> > always need to check the API contract (so to wrap in try/catch)
> 
> No, you don't. Nobody puts every method call in a try-catch today, just like
> nobody is going to be checking for a rejection on a promise for this kind of
> bug.
> 
> As a developer you never need to look at what the API will do when you use
> it incorrectly. You only need to use it correctly.
> 
> When you use it incorrectly, you should get an exception, the same way as
> when you call the wrong API.

FWIW, based on personal experience with Promises, I think that the current design leads to sadness very quickly. No sooner than I wrote 30 lines of code using Promises, I was already stumped by an accidental typo (like this, for instance http://jsbin.com/cosic/1/edit?js,console). FWIW :)
Comment 15 Dimitri Glazkov 2014-08-14 21:11:04 UTC
Kind people pointed out to me that I have (unintentionally) non sequitur-ed the thread. My apologies :)
Comment 16 Domenic Denicola 2014-08-14 21:15:50 UTC
Just to close that non-sequiter, that is only a problem in Chrome due to poor dev tools; if you run that sample in Firefox it shows the error wonderfully. In other words this is not an inherent problem in promises, but in dev tools.
Comment 17 Yutaka Hirano 2014-08-15 03:56:08 UTC
> In particular, if an API is called with bad arguments, then the exception should throw immediately, just like if it's name was typoed. The behaviour of:
>
>   foo.bar(-1); // only accepts positive numbers
>
> ...should be the same as the behaviour of:
>
>   foo.baz(1); // there's no "baz" method, it's called "bar"
Is the rule applied recursively?
For example, imagine we provide a Promise-returning function foo and it uses another function bar.

function foo(x, y) {
  if (typeof x !== 'number) { throw TypeError('x must be a number'); }
  bar(-x, y);
  return new Promise(...);
}

Should we enclose bar with try-catch?
Comment 18 Ian 'Hixie' Hickson 2014-08-18 20:28:23 UTC
I'm only talking about APIs here. How people want this to work for their own code is up to them.

The problem in comment 17 doesn't apply to APIs, because APIs are black boxes that by definition don't call other APIs incorrectly.
Comment 19 Yutaka Hirano 2014-08-29 00:51:16 UTC
IMO I prefer the current spec - returning a rejected Promise rather than throwing an error.

By the way, many APIs are currently using Promises. Chrome already returns a rejected Promise from APIs returning a Promise when there is an error, although the implementation is not perfect yet (see https://code.google.com/p/chromium/issues/detail?id=359386).
Changing the draft will affect many APIs (we thought that part of them for example WebCrypto were stable and "shipped" them). It would be great if we conclude this discussion soon.
Comment 20 Olli Pettay 2014-10-07 11:21:42 UTC
This inconsistency in rather horrible. 

var promise1 = object.foo(someParam);
// you should not do anything interesting here, since foo() call may even got
// wrong params. You're forced to wait for the promise to resolve/reject.

Error reporting really should happen early in certain cases, like in
param type validation.
Comment 21 Joshua Bell 2014-10-07 16:19:06 UTC
(In reply to Olli Pettay from comment #20)
> This inconsistency in rather horrible. 

And yet, we received exactly that complaint from Indexed DB users who ran into that issue with an API that works the way you propose (both synchronous and asynchronous failures):

var request = store.put(userdata, key);
request.onsuccess = ...;
request.onerror = ...;

"What do you mean I *also* need to catch exceptions from put() if the userdata is something that can't be cloned? I already have an error handler - make it consistent!"
Comment 22 Jonas Sicking (Not reading bugmail) 2014-10-07 17:37:54 UTC
I don't know of any APIs which have synchronous side-effects and which also return a promise. So you can't rely on anything having successfully happened on the line after the .foo() call anyway.
Comment 23 Tab Atkins Jr. 2014-10-07 17:45:48 UTC
(In reply to Jonas Sicking from comment #22)
> I don't know of any APIs which have synchronous side-effects and which also
> return a promise. So you can't rely on anything having successfully happened
> on the line after the .foo() call anyway.

Such an API would be a terrible design anyway, and would hopefully be flagged as such on review.
Comment 24 Mark S. Miller 2014-10-07 17:50:06 UTC
(In reply to Jonas Sicking from comment #22)
> I don't know of any APIs which have synchronous side-effects and which also
> return a promise. So you can't rely on anything having successfully happened
> on the line after the .foo() call anyway.

Just to clarify, I think that should be rephrased as "observably synchronous side effects". For example, the enqueue and dequeue methods of http://wiki.ecmascript.org/doku.php?id=strawman:concurrency#infinite_queue have sync side effects, but not observably so.
Comment 25 Olli Pettay 2014-10-07 17:55:06 UTC
(In reply to Jonas Sicking from comment #22)
> I don't know of any APIs which have synchronous side-effects and which also
> return a promise. So you can't rely on anything having successfully happened
> on the line after the .foo() call anyway.
Just passing wrong types of params to foo. It is not really a side-effect, but a basic type validation (or whatever term the spec uses).
And I'm not interested in the success case, I'm interested in the case when an error has occurred. That information should be given to the caller asap.
Comment 26 Tab Atkins Jr. 2014-10-07 18:10:22 UTC
(In reply to Olli Pettay from comment #25)
> (In reply to Jonas Sicking from comment #22)
> > I don't know of any APIs which have synchronous side-effects and which also
> > return a promise. So you can't rely on anything having successfully happened
> > on the line after the .foo() call anyway.
>
> Just passing wrong types of params to foo. It is not really a side-effect,
> but a basic type validation (or whatever term the spec uses).
> And I'm not interested in the success case, I'm interested in the case when
> an error has occurred. That information should be given to the caller asap.

Some "basic validations" can be performed synchronously, others can only be done async.  Having to split error-handling logic between sync and async isn't great, as it produces the two-locations annoyance that has already been brought up as an author complaint, and is often quite arbitrary.
Comment 27 Anne 2014-10-09 11:16:46 UTC
Marking WONTFIX per bug 25662 comment 4.