New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First-class error handling with `?` and `catch` #243

Merged
merged 10 commits into from Feb 5, 2016

Conversation

Projects
None yet
@glaebhoerl
Contributor

glaebhoerl commented Sep 16, 2014

After a detour through thinking about first-class checked exceptions, I've now circled back around and convinced myself that @aturon's original idea of an ? operator for propagating exceptions is actually brilliant and the perfect middle path. But I also want try..catch.

CLICKME

@netvl

This comment has been minimized.

Show comment
Hide comment
@netvl

netvl Sep 16, 2014

This is really great, but I see a potential problem. The RFC does not say anything about how to translate different errors into each other, and I think this is very important. For example, your library may work with several other libraries, each providing its own kind of error, and sometimes you would want to pass these errors to the users of your library. The most natural way is to wrap them into your own error enum, with different variants for different kinds of original errors.

But under this proposal there is no support for such patterns at all. Frankly, I don't know even in the slightest how this can be done in syntactically light and convenient way and even if it is possible in principle. Exceptions in other language do not have this problem mainly due to subtyping and having special Exception supertype, and these features are not present in Rust.

I'm afraid that this can be a very common use-case, and, unfortunatley, ? operator won't help with it at all.

netvl commented Sep 16, 2014

This is really great, but I see a potential problem. The RFC does not say anything about how to translate different errors into each other, and I think this is very important. For example, your library may work with several other libraries, each providing its own kind of error, and sometimes you would want to pass these errors to the users of your library. The most natural way is to wrap them into your own error enum, with different variants for different kinds of original errors.

But under this proposal there is no support for such patterns at all. Frankly, I don't know even in the slightest how this can be done in syntactically light and convenient way and even if it is possible in principle. Exceptions in other language do not have this problem mainly due to subtyping and having special Exception supertype, and these features are not present in Rust.

I'm afraid that this can be a very common use-case, and, unfortunatley, ? operator won't help with it at all.

@aturon

This comment has been minimized.

Show comment
Hide comment
Member

aturon commented Sep 16, 2014

@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

Ericson2314 Sep 17, 2014

Contributor

Ah, I was just thinking of making an RFC for "functional break" -- exactly your generalized return!

I am leery about most proposed syntactic sugars -- especially one as major as try..catch, and rather instead focus on using adding macros, or extending the macro system as necessary to give us what we want. (Hell, ideally I'd make bool an library-defined (enum) type, and even plain if..else a macro).

What I'd propose that people might actually agree with is simply adding the generalized break/return (Ideally one keyword could do everything, and the other would just be kept around for convenience), and making the try macro take an optional extra argument for a block lifetime:

match 'a: {
    try!('a, foo());
    try!('a, bar());
    Ok(()) // edit: added this so it would type check
} {
    None => ...,
     _ => ()
}

Not quite as pretty as try..catch, but on the other hand requires one only small addition to the language---and one that I'd argue more "rounds out" current features, since we already have loops as opposed to some system of mandatory tail calls, rather then delving it into new territory.

Besides my ascetic aversion to much syntactic sugar, I wonder whether the current demand for more control constructs will change with HKTs, and the design patterns they enable ;), and would vote for waiting until until we know the answer.

Contributor

Ericson2314 commented Sep 17, 2014

Ah, I was just thinking of making an RFC for "functional break" -- exactly your generalized return!

I am leery about most proposed syntactic sugars -- especially one as major as try..catch, and rather instead focus on using adding macros, or extending the macro system as necessary to give us what we want. (Hell, ideally I'd make bool an library-defined (enum) type, and even plain if..else a macro).

What I'd propose that people might actually agree with is simply adding the generalized break/return (Ideally one keyword could do everything, and the other would just be kept around for convenience), and making the try macro take an optional extra argument for a block lifetime:

match 'a: {
    try!('a, foo());
    try!('a, bar());
    Ok(()) // edit: added this so it would type check
} {
    None => ...,
     _ => ()
}

Not quite as pretty as try..catch, but on the other hand requires one only small addition to the language---and one that I'd argue more "rounds out" current features, since we already have loops as opposed to some system of mandatory tail calls, rather then delving it into new territory.

Besides my ascetic aversion to much syntactic sugar, I wonder whether the current demand for more control constructs will change with HKTs, and the design patterns they enable ;), and would vote for waiting until until we know the answer.

Show outdated Hide outdated active/0000-trait-based-exception-handling.md
try {
foo()?.bar()?
} catch e {

This comment has been minimized.

@P1start

P1start Sep 17, 2014

Contributor

Can’t e just be an arbitrary refutable pattern here, and allow multiple catch arms? So the example below becomes this:

try {
    foo()?.bar()?
} catch Red(rex) {
    baz(rex)
} catch Blue(bex) {
    quux(bex)
}

That way you only need one type of catch block and there’s less rightward drift.

@P1start

P1start Sep 17, 2014

Contributor

Can’t e just be an arbitrary refutable pattern here, and allow multiple catch arms? So the example below becomes this:

try {
    foo()?.bar()?
} catch Red(rex) {
    baz(rex)
} catch Blue(bex) {
    quux(bex)
}

That way you only need one type of catch block and there’s less rightward drift.

This comment has been minimized.

@Ericson2314

Ericson2314 Sep 17, 2014

Contributor

To me that syntax looks like if-let..else, and implies no guarantee of catching all cases. Catch should handle all variants---the alternative gives me bad memories of Java's RuntimeException.

@Ericson2314

Ericson2314 Sep 17, 2014

Contributor

To me that syntax looks like if-let..else, and implies no guarantee of catching all cases. Catch should handle all variants---the alternative gives me bad memories of Java's RuntimeException.

This comment has been minimized.

@P1start

P1start Sep 17, 2014

Contributor

Just make it like match—all cases have to be covered. Just because it looks vaguely like ‘iflet’ doesn’t mean it has to behave like it. I’d immediately assume that all errors have to be handled anyway, regardless of syntax.

@P1start

P1start Sep 17, 2014

Contributor

Just make it like match—all cases have to be covered. Just because it looks vaguely like ‘iflet’ doesn’t mean it has to behave like it. I’d immediately assume that all errors have to be handled anyway, regardless of syntax.

This comment has been minimized.

@glaebhoerl

glaebhoerl Oct 3, 2014

Contributor

I think this is also a reasonable choice. But I think I prefer the match-like syntax because it better matches the actual behavior and meaning, i.e. it makes the reader think of match, rather than of try-catch-catch from other languages, and this is in fact the correct intuition. The alternative would repurpose familiar syntax to mean something similar but significantly different, which I'm a little bit uneasy about.

(But again I think both are basically fine, I'm just explaining my preference.)

@glaebhoerl

glaebhoerl Oct 3, 2014

Contributor

I think this is also a reasonable choice. But I think I prefer the match-like syntax because it better matches the actual behavior and meaning, i.e. it makes the reader think of match, rather than of try-catch-catch from other languages, and this is in fact the correct intuition. The alternative would repurpose familiar syntax to mean something similar but significantly different, which I'm a little bit uneasy about.

(But again I think both are basically fine, I'm just explaining my preference.)

This comment has been minimized.

@Razican

Razican Oct 4, 2015

IMHO, if something should behave like match, it should look like match.

@Razican

Razican Oct 4, 2015

IMHO, if something should behave like match, it should look like match.

@nrc nrc assigned brson and aturon and unassigned brson Sep 18, 2014

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Oct 3, 2014

Member

@glaebhoerl

First of all, thanks for writing another beautiful and thorough RFC. It's always a pleasure to read these.

The ideas you're proposing help overcome one of my worries with the initial ? proposal, namely that it was tied to exiting the function. While this is the common case in Rust code today (probably due, in part, to try!), and you can always factor your code into smaller functions to make it work, the extra flexibility in this RFC seems both appealing and not very complicated.

I also wholeheartedly agree that this design finds a middle ground between completely implicit propagation (traditional exceptions) and completely explicit propagation (Result without try!). It recovers much of the ergonomics of implicit propagation, while keeping control-flow jumps explicit.

I think that throw and throws are promising as well. My main worry there is that throws somewhat muddies the integration with return types, for better or for worse. I suspect it could and should be integrated with closure type syntax, if we decide to include it.

I'm undecided on the Carrier question, in part because we're (still!) trying to finalize conventions around the use of Result and Option.

Have you looked at the error interoperation RFC? Part of the goal there was to allow different error types to be automatically converted when using try!. The reason that works, however, is that the function's return type (and hence the target error type) is always explicitly written. This property would no longer hold with try-catch blocks, so I'm not sure how or whether automatic conversion would apply there. I'd be interested to get your thoughts.

So, on the whole I'm pretty enthusiastic about these ideas, and I think that if we add ?, we should do so in the form proposed here.

However, as you know, at the moment we're setting a pretty high standard for RFC acceptance: we're relentlessly focusing on what is needed for a solid 1.0 release (e.g., backcompat hazards or proposals that are needed for overall usability or coherence for 1.0). After the release, we will of course start considering more "nice-to-have" features.

Personally, I feel that having a solid error-handling story is an important aspect of 1.0, which is part of why I've been pushing on various related aspects (both conventions and sugar). It's not clear to me whether try! is enough to make a good first impression, but we also have our hands rather full implementing already-accepted features. It might be possible to accept this RFC but explicitly not as a 1.0 blocker; I'm not sure, but I'll discuss it with the team.

Two final questions:

  • If we have a ? operator as proposed here for error handling, does that change your opinion about !? What if, independently, macro syntax was changed to use @ (which has been separately proposed; I know you're not fond of that, but hypothetically?)

  • Do you have thoughts on the minimal steps we can take pre-1.0 to ensure that we can implement this feature later? The main issue seems to be the try keyword, which of course conflicts with the try! macro. It's probably feasible (if confusing/ugly) to allow both, i.e. to treat ! as part of the identifier. Alternatively, we could rename try! -- any suggestions?

    Actually, one possibility would be to implement ? as it was originally proposed (i.e., limited to returning from the current function) and then dump or rename try!. Rust code written in that style would continue to feel idiomatic if/when the rest of this RFC was implemented.

(As an aside, Standard ML at least has a form of try/catch that yields an expression.)

Member

aturon commented Oct 3, 2014

@glaebhoerl

First of all, thanks for writing another beautiful and thorough RFC. It's always a pleasure to read these.

The ideas you're proposing help overcome one of my worries with the initial ? proposal, namely that it was tied to exiting the function. While this is the common case in Rust code today (probably due, in part, to try!), and you can always factor your code into smaller functions to make it work, the extra flexibility in this RFC seems both appealing and not very complicated.

I also wholeheartedly agree that this design finds a middle ground between completely implicit propagation (traditional exceptions) and completely explicit propagation (Result without try!). It recovers much of the ergonomics of implicit propagation, while keeping control-flow jumps explicit.

I think that throw and throws are promising as well. My main worry there is that throws somewhat muddies the integration with return types, for better or for worse. I suspect it could and should be integrated with closure type syntax, if we decide to include it.

I'm undecided on the Carrier question, in part because we're (still!) trying to finalize conventions around the use of Result and Option.

Have you looked at the error interoperation RFC? Part of the goal there was to allow different error types to be automatically converted when using try!. The reason that works, however, is that the function's return type (and hence the target error type) is always explicitly written. This property would no longer hold with try-catch blocks, so I'm not sure how or whether automatic conversion would apply there. I'd be interested to get your thoughts.

So, on the whole I'm pretty enthusiastic about these ideas, and I think that if we add ?, we should do so in the form proposed here.

However, as you know, at the moment we're setting a pretty high standard for RFC acceptance: we're relentlessly focusing on what is needed for a solid 1.0 release (e.g., backcompat hazards or proposals that are needed for overall usability or coherence for 1.0). After the release, we will of course start considering more "nice-to-have" features.

Personally, I feel that having a solid error-handling story is an important aspect of 1.0, which is part of why I've been pushing on various related aspects (both conventions and sugar). It's not clear to me whether try! is enough to make a good first impression, but we also have our hands rather full implementing already-accepted features. It might be possible to accept this RFC but explicitly not as a 1.0 blocker; I'm not sure, but I'll discuss it with the team.

Two final questions:

  • If we have a ? operator as proposed here for error handling, does that change your opinion about !? What if, independently, macro syntax was changed to use @ (which has been separately proposed; I know you're not fond of that, but hypothetically?)

  • Do you have thoughts on the minimal steps we can take pre-1.0 to ensure that we can implement this feature later? The main issue seems to be the try keyword, which of course conflicts with the try! macro. It's probably feasible (if confusing/ugly) to allow both, i.e. to treat ! as part of the identifier. Alternatively, we could rename try! -- any suggestions?

    Actually, one possibility would be to implement ? as it was originally proposed (i.e., limited to returning from the current function) and then dump or rename try!. Rust code written in that style would continue to feel idiomatic if/when the rest of this RFC was implemented.

(As an aside, Standard ML at least has a form of try/catch that yields an expression.)

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 3, 2014

Contributor

OK, so I read this over. This is pretty cool stuff. If I'm not mistaken, the throws syntax (and the Carrier trait) are backwards compatible extensions, right? If so, I'd probably prefer to move slowly and leave those out for now.

There are some things I really like about this proposal:

It means that the role of the try keyword is more analogous to try as traditionally used (it defines the scope of error-handling, which try! obviously doesn't do).

This also seems to give you roughly all the things you might want to do in a fairly compact way:

  • foo? --> try!(foo)
  • try foo?.bar --> foo.map(bar)
  • try foo?.bar? --> foo.and_then(bar)
Contributor

nikomatsakis commented Oct 3, 2014

OK, so I read this over. This is pretty cool stuff. If I'm not mistaken, the throws syntax (and the Carrier trait) are backwards compatible extensions, right? If so, I'd probably prefer to move slowly and leave those out for now.

There are some things I really like about this proposal:

It means that the role of the try keyword is more analogous to try as traditionally used (it defines the scope of error-handling, which try! obviously doesn't do).

This also seems to give you roughly all the things you might want to do in a fairly compact way:

  • foo? --> try!(foo)
  • try foo?.bar --> foo.map(bar)
  • try foo?.bar? --> foo.and_then(bar)
@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 3, 2014

Contributor

@aturon points out that for 1.0 staging we can add ? as a synonym for try! for now, and add try-catch keywords in later. This is probably worth nothing in the RFC.

Contributor

nikomatsakis commented Oct 3, 2014

@aturon points out that for 1.0 staging we can add ? as a synonym for try! for now, and add try-catch keywords in later. This is probably worth nothing in the RFC.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Oct 3, 2014

Member

This also seems to give you roughly all the things you might want to do in a fairly compact way:

  • foo? --> try!(foo)
  • try foo?.bar --> foo.map(bar)
  • try foo?.bar? --> foo.and_then(bar)

Note that, in particular, this notation subsumes:

  • do notation when applied to the Error monad,
  • Swift's ?
  • try! that returns to the function boundary, which is not part of either of the two above.

I believe that even if we added monadic notation at some later time (which has many problems of its own), we would still profit from this specialized syntax for propagating and catching errors in a very lightweight way -- and a way that largely matches expectations when coming from a wide variety of other languages.

Member

aturon commented Oct 3, 2014

This also seems to give you roughly all the things you might want to do in a fairly compact way:

  • foo? --> try!(foo)
  • try foo?.bar --> foo.map(bar)
  • try foo?.bar? --> foo.and_then(bar)

Note that, in particular, this notation subsumes:

  • do notation when applied to the Error monad,
  • Swift's ?
  • try! that returns to the function boundary, which is not part of either of the two above.

I believe that even if we added monadic notation at some later time (which has many problems of its own), we would still profit from this specialized syntax for propagating and catching errors in a very lightweight way -- and a way that largely matches expectations when coming from a wide variety of other languages.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 3, 2014

Contributor

Also, I think I vaguely prefer the multiple catch arm syntax that @P1start suggested, since it resembles what other languages do, and because it unifies the two cases, though it obviously resembles match "less".

Contributor

nikomatsakis commented Oct 3, 2014

Also, I think I vaguely prefer the multiple catch arm syntax that @P1start suggested, since it resembles what other languages do, and because it unifies the two cases, though it obviously resembles match "less".

@arcto

This comment has been minimized.

Show comment
Hide comment
@arcto

arcto Oct 3, 2014

I really like the semantics and the ergonomics that this proposal would bring.

However, I can see this being used for a lot more than just exceptional failures. So I'm a bit unsure about the terminology and the naming of some of the constructs.

arcto commented Oct 3, 2014

I really like the semantics and the ergonomics that this proposal would bring.

However, I can see this being used for a lot more than just exceptional failures. So I'm a bit unsure about the terminology and the naming of some of the constructs.

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Oct 4, 2014

Contributor

@Ericson2314 Feel free to submit a proposal for "functional break" if you like! I'm working on other things at the moment. (As discussed on discourse it seems like break would be the more appropriate choice, rather than return as I chose here.)


@aturon Thank you.

I think that throw and throws are promising as well. My main worry there is that throws somewhat muddies the integration with return types, for better or for worse. I suspect it could and should be integrated with closure type syntax, if we decide to include it.

Can you sketch how it might be implemented? I haven't thought about it deeply, but I wouldn't be surprised if it turned out to require higher-rank types for full generality. It's also not clear to me what the use case would be. (If you have a concrete fn that throws, it's polymorphic in the carrier, so it can be used at a closure type returning any concrete carrier. When or why would you want the closure itself to be polymorphic in the carrier?)

I'm undecided on the Carrier question, in part because we're (still!) trying to finalize conventions around the use of Result and Option.

Er, sorry: which Carrier question?

Have you looked at the error interoperation RFC? Part of the goal there was to allow different error types to be automatically converted when using try!. The reason that works, however, is that the function's return type (and hence the target error type) is always explicitly written. This property would no longer hold with try-catch blocks, so I'm not sure how or whether automatic conversion would apply there. I'd be interested to get your thoughts.

I have read it. With regards to the specific question as phrased, I suspect that the appropriate type for the try block can usually be inferred from the contents of the catch block. Do you have contrary examples? (But it's also not clear to me when you would want to do this -- wrapping in Box<Error> is something you would do to hide the specific error types used by upstream dependencies from downstream clients. But that's if you are propagating the errors. If you're handling them yourself with try..catch, why wouldn't you just inspect them directly? Why would you want to hide their types from yourself?)

Perhaps more importantly, the design as formulated in this RFC would preclude that kind of automatic conversion happening with the ? operator. Personally, I think that's a good thing. I recognize that the ergonomics of interfacing different errors types are important, but I would be deeply uncomfortable with baking this kind of ad-hoc special casing into the guts of the language. It's not even clear that this is the best way to do it, and having strong laws and ability to reason about code is much more important. ("Those who would give up essential Guarantees, to purchase a little temporary Convenience, deserve neither Guarantees nor Convenience." --Ben Franklin) I think a reasonable path forward would be to have a macro, separate from ?, which does do the automatic conversion (for instance rethrow!; I'm not sure if that's the best name). Then ? would have nice properties, the ergonomics of error-conversion would not be worse than proposed in the error interoperation RFC (which also assumed a macro), and it would be clear where automatic conversion may or may not happen.

However, as you know, at the moment we're setting a pretty high standard for RFC acceptance: we're relentlessly focusing on what is needed for a solid 1.0 release [...]

That's fine; this RFC was primarily intended to inform the ongoing debate about error handling. (But as far as I'm aware, an accepted RFC also does not imply that it has to, or will be, implemented before 1.0.)

Do you have thoughts on the minimal steps we can take pre-1.0 to ensure that we can implement this feature later?

I agree that the best approach would be to replace the try! macro with an ? operator restricted to the Result type and returning from the current function. This is a strict subset of the functionality described by the RFC, so I don't think it requires modifying the RFC in any way; it can just be considered "partially implemented" at that point.

If we have a ? operator as proposed here for error handling, does that change your opinion about !? What if, independently, macro syntax was changed to use @ (which has been separately proposed; I know you're not fond of that, but hypothetically?)

It doesn't. My opinion about the ! operator is that it's "probably a bad idea". (It has a kind of nice symmetry with ?, but that doesn't make it wise.) More importantly however, I think that adding it would be premature and that doing so is not supported by the weight of the available evidence. The idea that convention-following APIs would be too cumbersome to use without it is highly speculative, especially given that other ergonomically significant features, such as in this RFC, are not yet available. There is also at least one indication that it is possible to avoid the need for it entirely. If, after half a year or so, experience shows that living without ! is still too painful, then a compelling case could possibly be made. But not now. (And I would be surprised.)

The fact that it also entails losing the ! syntax for macros is just anti-icing on the anti-cake.

(If we decide that we don't want an ! operator after all, can we then have it back for macros? My guess is no, because having two equivalent syntaxes is undesirable.)


@nikomatsakis

try foo?.bar --> foo.map(bar)
try foo?.bar? --> foo.and_then(bar)

I can't help but notice that you left off the braces. It would be nice to allow this, but would it not run into the same ambiguities as if..else? (This might also be connected to the choice of catch syntax.)

Also, I think I vaguely prefer the multiple catch arm syntax that @P1start suggested, since it resembles what other languages do, and because it unifies the two cases, though it obviously resembles match "less".

As in my comment to @P1start, I think the fact that it unifies the cases is nice, but having similar things look similar and different things look different feels more important.


@arcto

However, I can see this being used for a lot more than just exceptional failures. So I'm a bit unsure about the terminology and the naming of some of the constructs.

I do think we can dispense with some of the stigmas and mythology surrounding exception handling in other languages, e.g. the hair-splitting about the meaning of "truly exceptional circumstances" and so on, which are due to the fact that they have to choose between error codes and exceptions, while here they are unified, and that their exceptions have various significant and undesirable aspects (e.g. not being tracked in the type system), while these don't. It can be used as just another control flow construct, and I think that's fine. Familiar syntax is still worthwhile. (See also e.g. enum for ADTs.)

Contributor

glaebhoerl commented Oct 4, 2014

@Ericson2314 Feel free to submit a proposal for "functional break" if you like! I'm working on other things at the moment. (As discussed on discourse it seems like break would be the more appropriate choice, rather than return as I chose here.)


@aturon Thank you.

I think that throw and throws are promising as well. My main worry there is that throws somewhat muddies the integration with return types, for better or for worse. I suspect it could and should be integrated with closure type syntax, if we decide to include it.

Can you sketch how it might be implemented? I haven't thought about it deeply, but I wouldn't be surprised if it turned out to require higher-rank types for full generality. It's also not clear to me what the use case would be. (If you have a concrete fn that throws, it's polymorphic in the carrier, so it can be used at a closure type returning any concrete carrier. When or why would you want the closure itself to be polymorphic in the carrier?)

I'm undecided on the Carrier question, in part because we're (still!) trying to finalize conventions around the use of Result and Option.

Er, sorry: which Carrier question?

Have you looked at the error interoperation RFC? Part of the goal there was to allow different error types to be automatically converted when using try!. The reason that works, however, is that the function's return type (and hence the target error type) is always explicitly written. This property would no longer hold with try-catch blocks, so I'm not sure how or whether automatic conversion would apply there. I'd be interested to get your thoughts.

I have read it. With regards to the specific question as phrased, I suspect that the appropriate type for the try block can usually be inferred from the contents of the catch block. Do you have contrary examples? (But it's also not clear to me when you would want to do this -- wrapping in Box<Error> is something you would do to hide the specific error types used by upstream dependencies from downstream clients. But that's if you are propagating the errors. If you're handling them yourself with try..catch, why wouldn't you just inspect them directly? Why would you want to hide their types from yourself?)

Perhaps more importantly, the design as formulated in this RFC would preclude that kind of automatic conversion happening with the ? operator. Personally, I think that's a good thing. I recognize that the ergonomics of interfacing different errors types are important, but I would be deeply uncomfortable with baking this kind of ad-hoc special casing into the guts of the language. It's not even clear that this is the best way to do it, and having strong laws and ability to reason about code is much more important. ("Those who would give up essential Guarantees, to purchase a little temporary Convenience, deserve neither Guarantees nor Convenience." --Ben Franklin) I think a reasonable path forward would be to have a macro, separate from ?, which does do the automatic conversion (for instance rethrow!; I'm not sure if that's the best name). Then ? would have nice properties, the ergonomics of error-conversion would not be worse than proposed in the error interoperation RFC (which also assumed a macro), and it would be clear where automatic conversion may or may not happen.

However, as you know, at the moment we're setting a pretty high standard for RFC acceptance: we're relentlessly focusing on what is needed for a solid 1.0 release [...]

That's fine; this RFC was primarily intended to inform the ongoing debate about error handling. (But as far as I'm aware, an accepted RFC also does not imply that it has to, or will be, implemented before 1.0.)

Do you have thoughts on the minimal steps we can take pre-1.0 to ensure that we can implement this feature later?

I agree that the best approach would be to replace the try! macro with an ? operator restricted to the Result type and returning from the current function. This is a strict subset of the functionality described by the RFC, so I don't think it requires modifying the RFC in any way; it can just be considered "partially implemented" at that point.

If we have a ? operator as proposed here for error handling, does that change your opinion about !? What if, independently, macro syntax was changed to use @ (which has been separately proposed; I know you're not fond of that, but hypothetically?)

It doesn't. My opinion about the ! operator is that it's "probably a bad idea". (It has a kind of nice symmetry with ?, but that doesn't make it wise.) More importantly however, I think that adding it would be premature and that doing so is not supported by the weight of the available evidence. The idea that convention-following APIs would be too cumbersome to use without it is highly speculative, especially given that other ergonomically significant features, such as in this RFC, are not yet available. There is also at least one indication that it is possible to avoid the need for it entirely. If, after half a year or so, experience shows that living without ! is still too painful, then a compelling case could possibly be made. But not now. (And I would be surprised.)

The fact that it also entails losing the ! syntax for macros is just anti-icing on the anti-cake.

(If we decide that we don't want an ! operator after all, can we then have it back for macros? My guess is no, because having two equivalent syntaxes is undesirable.)


@nikomatsakis

try foo?.bar --> foo.map(bar)
try foo?.bar? --> foo.and_then(bar)

I can't help but notice that you left off the braces. It would be nice to allow this, but would it not run into the same ambiguities as if..else? (This might also be connected to the choice of catch syntax.)

Also, I think I vaguely prefer the multiple catch arm syntax that @P1start suggested, since it resembles what other languages do, and because it unifies the two cases, though it obviously resembles match "less".

As in my comment to @P1start, I think the fact that it unifies the cases is nice, but having similar things look similar and different things look different feels more important.


@arcto

However, I can see this being used for a lot more than just exceptional failures. So I'm a bit unsure about the terminology and the naming of some of the constructs.

I do think we can dispense with some of the stigmas and mythology surrounding exception handling in other languages, e.g. the hair-splitting about the meaning of "truly exceptional circumstances" and so on, which are due to the fact that they have to choose between error codes and exceptions, while here they are unified, and that their exceptions have various significant and undesirable aspects (e.g. not being tracked in the type system), while these don't. It can be used as just another control flow construct, and I think that's fine. Familiar syntax is still worthwhile. (See also e.g. enum for ADTs.)

Show outdated Hide outdated active/0000-trait-based-exception-handling.md
exceptions) matches what code might look like in a language with native
exceptions.
(This could potentially be extended to allow writing `throws` clauses on `fn`

This comment has been minimized.

@reem

reem Oct 7, 2014

I actually don't think we can have one without the other - it would make using throws in a declaration a very leaky abstraction if I had to find out the expanded type if I wanted to store that function anywhere, which is especially true for closures.

@reem

reem Oct 7, 2014

I actually don't think we can have one without the other - it would make using throws in a declaration a very leaky abstraction if I had to find out the expanded type if I wanted to store that function anywhere, which is especially true for closures.

It is possible to carry the exception handling analogy further and also add
`throw` and `throws` constructs.
`throw` is very simple: `throw EXPR` is essentially the same thing as

This comment has been minimized.

@reem

reem Oct 7, 2014

I want to like this a lot (it's much cleaner syntactically) but I fear the additional (apparent, if not real) complexity.

@reem

reem Oct 7, 2014

I want to like this a lot (it's much cleaner syntactically) but I fear the additional (apparent, if not real) complexity.

Show outdated Hide outdated active/0000-trait-based-exception-handling.md
A `throws` clause on a function:
fn foo(arg; Foo) -> Bar throws Baz { ... }

This comment has been minimized.

@reem

reem Oct 7, 2014

Looking at the expanded type below, I think this is a lot nicer and probably worth including if we go down this route.

@reem

reem Oct 7, 2014

Looking at the expanded type below, I think this is a lot nicer and probably worth including if we go down this route.

@hatahet

This comment has been minimized.

Show comment
Hide comment
@hatahet

hatahet Oct 7, 2014

I came across a couple of articles and thought they might be worth considering:

I realize though this is not 100% similar to C++'s (unchecked) or Java's (checked) exceptions.

hatahet commented Oct 7, 2014

I came across a couple of articles and thought they might be worth considering:

I realize though this is not 100% similar to C++'s (unchecked) or Java's (checked) exceptions.

@brson

This comment has been minimized.

Show comment
Hide comment
@brson

brson Oct 8, 2014

Contributor

While I think this is an interesting and novel proposal, I am concerned about re-purposing the exception handling terminology ('try', 'catch', 'throw', 'exception').

  • Exception handling has baggage, and people will draw conclusions based on the words alone.
  • The mechanism is different from typical exceptions, has different performance characteristics and behavior.
  • Rust already also includes much of the traditional exception handling mechanism, but in a non-traditional form, under a different name (panic), so calling something else 'exceptions' makes the issue rather muddy.
  • Finally, this is such a cool idea that we might want to completely own it.
Contributor

brson commented Oct 8, 2014

While I think this is an interesting and novel proposal, I am concerned about re-purposing the exception handling terminology ('try', 'catch', 'throw', 'exception').

  • Exception handling has baggage, and people will draw conclusions based on the words alone.
  • The mechanism is different from typical exceptions, has different performance characteristics and behavior.
  • Rust already also includes much of the traditional exception handling mechanism, but in a non-traditional form, under a different name (panic), so calling something else 'exceptions' makes the issue rather muddy.
  • Finally, this is such a cool idea that we might want to completely own it.
@suhr

This comment has been minimized.

Show comment
Hide comment
@suhr

suhr Oct 8, 2014

Will it make a mess when HKT is added to the language?

suhr commented Oct 8, 2014

Will it make a mess when HKT is added to the language?

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 8, 2014

Contributor

@glaebhoerl you're correct about try {} vs try expr. Having to put braces is kind of a downer, but I agree that to do otherwise is inconsistent with our if/else story.

@brson interesting point. I wonder what would be a compelling alternative set of terms.

Contributor

nikomatsakis commented Oct 8, 2014

@glaebhoerl you're correct about try {} vs try expr. Having to put braces is kind of a downer, but I agree that to do otherwise is inconsistent with our if/else story.

@brson interesting point. I wonder what would be a compelling alternative set of terms.

@arthurprs

This comment has been minimized.

Show comment
Hide comment
@arthurprs

arthurprs Oct 8, 2014

I second @brson thoughts. If this gets incorporated it'd be best to move away from "exception handling"-ish descriptions and stick with "error handling"

arthurprs commented Oct 8, 2014

I second @brson thoughts. If this gets incorporated it'd be best to move away from "exception handling"-ish descriptions and stick with "error handling"

@huonw

This comment has been minimized.

Show comment
Hide comment
@huonw

huonw Oct 8, 2014

Member

I can't help but notice that you left off the braces. It would be nice to allow this, but would it not run into the same ambiguities as if..else? (This might also be connected to the choice of catch syntax.)

@glaebhoerl you're correct about try {} vs try expr. Having to put braces is kind of a downer, but I agree that to do otherwise is inconsistent with our if/else story.

I'm missing something here: don't we require braces because it would be if cond expr else, that is, there's two adjacent expressions and that doesn't work. For try (AIUI) it is just try expr catch i.e. one expression and so perfectly OK from a grammar stand-point?

Also, it's not that inconsistent with if etc; we don't require delimiters around the condition of a if/while, or match head.

Member

huonw commented Oct 8, 2014

I can't help but notice that you left off the braces. It would be nice to allow this, but would it not run into the same ambiguities as if..else? (This might also be connected to the choice of catch syntax.)

@glaebhoerl you're correct about try {} vs try expr. Having to put braces is kind of a downer, but I agree that to do otherwise is inconsistent with our if/else story.

I'm missing something here: don't we require braces because it would be if cond expr else, that is, there's two adjacent expressions and that doesn't work. For try (AIUI) it is just try expr catch i.e. one expression and so perfectly OK from a grammar stand-point?

Also, it's not that inconsistent with if etc; we don't require delimiters around the condition of a if/while, or match head.

@arcto

This comment has been minimized.

Show comment
Hide comment
@arcto

arcto Oct 8, 2014

I'm not going to defend this position, but short-circuiting by using an early break/return or ?-operator can be seen as a more general feature than both error handling and exception handling. Suppose, for instance, that the "carrier" is an Option. A value of None is not necessarily an error.

arcto commented Oct 8, 2014

I'm not going to defend this position, but short-circuiting by using an early break/return or ?-operator can be seen as a more general feature than both error handling and exception handling. Suppose, for instance, that the "carrier" is an Option. A value of None is not necessarily an error.

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Oct 8, 2014

Contributor

@huonw IIRC we require braces to avoid the common mistake from other C-like languages where an else clause is interpreted by the compiler as belonging to a different if than which the author intended.

@brson

  • Exception handling has baggage, and people will draw conclusions based on the words alone.

This is a reasonable point. I was thinking of aiming for the title of "the language that finally got exceptions right".

The goal of this naming scheme is to provide a good intuition for the constructs. "It works like try-catch in other languages, except you need an explicit question mark to propagate". (Whereas otherwise it could end up being "that weird error handling construct that Rust has".) I think there's enough variety among exception handling implementations in existing languages that our deviations can fit under the same roof. (Haskell also uses exception handling terminology for their implementation, which has much more in common with this one than with others.)

But even that is probably overexplaining it. They have these names because the try-catch of existing languages is where the idea came from in the first place.

(Just as a thought experiment for anyone reading -- do you think it would have been easier or harder to grok the intended meaning and usage of these proposed constructs if they had been presented using different vocabulary, not connected to exception handling?)

  • The mechanism is different from typical exceptions, has different performance characteristics and behavior.
  • Rust already also includes much of the traditional exception handling mechanism, but in a non-traditional form, under a different name (panic), so calling something else 'exceptions' makes the issue rather muddy.

I think people are more attuned to meaning than to mechanism. If they're not meant to be caught then they're not really exceptions (accordingly, we call them panics), even if it involves unwinding.

All that said this is all very theoretical. My feeling is that trying to go with different names just for the sake of being different is likely to end up being more confusing, not less. But this could be assessed much more straightforwardly for a concrete set of alternative names.

Contributor

glaebhoerl commented Oct 8, 2014

@huonw IIRC we require braces to avoid the common mistake from other C-like languages where an else clause is interpreted by the compiler as belonging to a different if than which the author intended.

@brson

  • Exception handling has baggage, and people will draw conclusions based on the words alone.

This is a reasonable point. I was thinking of aiming for the title of "the language that finally got exceptions right".

The goal of this naming scheme is to provide a good intuition for the constructs. "It works like try-catch in other languages, except you need an explicit question mark to propagate". (Whereas otherwise it could end up being "that weird error handling construct that Rust has".) I think there's enough variety among exception handling implementations in existing languages that our deviations can fit under the same roof. (Haskell also uses exception handling terminology for their implementation, which has much more in common with this one than with others.)

But even that is probably overexplaining it. They have these names because the try-catch of existing languages is where the idea came from in the first place.

(Just as a thought experiment for anyone reading -- do you think it would have been easier or harder to grok the intended meaning and usage of these proposed constructs if they had been presented using different vocabulary, not connected to exception handling?)

  • The mechanism is different from typical exceptions, has different performance characteristics and behavior.
  • Rust already also includes much of the traditional exception handling mechanism, but in a non-traditional form, under a different name (panic), so calling something else 'exceptions' makes the issue rather muddy.

I think people are more attuned to meaning than to mechanism. If they're not meant to be caught then they're not really exceptions (accordingly, we call them panics), even if it involves unwinding.

All that said this is all very theoretical. My feeling is that trying to go with different names just for the sake of being different is likely to end up being more confusing, not less. But this could be assessed much more straightforwardly for a concrete set of alternative names.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 8, 2014

Contributor

On Wed, Oct 08, 2014 at 06:04:41AM -0700, Huon Wilson wrote:

@glaebhoerl you're correct about try {} vs try expr. Having to put braces is kind of a downer, but I agree that to do otherwise is inconsistent with our if/else story.

I'm missing something here: don't we require braces because it would be if cond expr else, that is, there's two adjacent expressions and that doesn't work. For try (AIUI) it is just try expr catch i.e. one expression and so perfectly OK from a grammar stand-point?

Well, there are multiple reasons to require braces. One of them is that it lets you drop the parens, yes. The other is that it avoids ambiguity for cases like

if (cond1)
    if (cond2)
        something
else
    something_else

in this case, the else is associated with the inner if, despite what the indentation suggests.

You can certainly construct analogous situations with the propose try/catch. One fix would be to permit dropping the {} if there is no catch.

Contributor

nikomatsakis commented Oct 8, 2014

On Wed, Oct 08, 2014 at 06:04:41AM -0700, Huon Wilson wrote:

@glaebhoerl you're correct about try {} vs try expr. Having to put braces is kind of a downer, but I agree that to do otherwise is inconsistent with our if/else story.

I'm missing something here: don't we require braces because it would be if cond expr else, that is, there's two adjacent expressions and that doesn't work. For try (AIUI) it is just try expr catch i.e. one expression and so perfectly OK from a grammar stand-point?

Well, there are multiple reasons to require braces. One of them is that it lets you drop the parens, yes. The other is that it avoids ambiguity for cases like

if (cond1)
    if (cond2)
        something
else
    something_else

in this case, the else is associated with the inner if, despite what the indentation suggests.

You can certainly construct analogous situations with the propose try/catch. One fix would be to permit dropping the {} if there is no catch.

@brson

This comment has been minimized.

Show comment
Hide comment
@brson

brson Oct 8, 2014

Contributor

@glaebhoerl

(Just as a thought experiment for anyone reading -- do you think it would have been easier or harder to grok the intended meaning and usage of these proposed constructs if they had been presented using different vocabulary, not connected to exception handling?)

I do think try/catch provides the right intuition, and that's an argument that has won some (not all) naming debates in Rust in the past (and we even have several things called 'try' because it's such an intuitive concept). The word 'exception' is definitely the most worrisome here, though not having 'exceptions' but having try/catch requires some explanation.

We're probably going to end up with a long FAQ entry explaining the subtleties of exception and error handling as relates to Rust no matter what :)

Contributor

brson commented Oct 8, 2014

@glaebhoerl

(Just as a thought experiment for anyone reading -- do you think it would have been easier or harder to grok the intended meaning and usage of these proposed constructs if they had been presented using different vocabulary, not connected to exception handling?)

I do think try/catch provides the right intuition, and that's an argument that has won some (not all) naming debates in Rust in the past (and we even have several things called 'try' because it's such an intuitive concept). The word 'exception' is definitely the most worrisome here, though not having 'exceptions' but having try/catch requires some explanation.

We're probably going to end up with a long FAQ entry explaining the subtleties of exception and error handling as relates to Rust no matter what :)

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 8, 2014

Contributor

cc @wycats, this will interest you

Contributor

nikomatsakis commented Oct 8, 2014

cc @wycats, this will interest you

@wycats

This comment has been minimized.

Show comment
Hide comment
@wycats

wycats Oct 8, 2014

Contributor

@nikomatsakis thanks for tagging me in.

I'm still digesting all of this. My initial knee-jerk reaction is about terminology: try may offer some of the right intuitions, but is also drags along the intuition of "anyone downstream from me can produce an exception, and I may not be able to tell". Very few people derive their intuitions from a language with checked exceptions only, and at least for me, "exception" and try {} have a very strong intuition of unpredictability, and throwing from an arbitrary downstream.

Contributor

wycats commented Oct 8, 2014

@nikomatsakis thanks for tagging me in.

I'm still digesting all of this. My initial knee-jerk reaction is about terminology: try may offer some of the right intuitions, but is also drags along the intuition of "anyone downstream from me can produce an exception, and I may not be able to tell". Very few people derive their intuitions from a language with checked exceptions only, and at least for me, "exception" and try {} have a very strong intuition of unpredictability, and throwing from an arbitrary downstream.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Oct 9, 2014

Contributor

I was thinking that, without the Carrier trait ideas, the role of the throw and catch keywords are unclear. That is, you can simplify the proposal to a ? operator and a try keyword that defines its scope, right? The other keywords are basically there to allow the code to be generic with respect to the "carrier" type.

Contributor

nikomatsakis commented Oct 9, 2014

I was thinking that, without the Carrier trait ideas, the role of the throw and catch keywords are unclear. That is, you can simplify the proposal to a ? operator and a try keyword that defines its scope, right? The other keywords are basically there to allow the code to be generic with respect to the "carrier" type.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Oct 13, 2014

Member

@glaebhoerl

Here are some more thoughts.

First, I think we should skip Carrier and bind directly to Result to start with. This will push people to use Result for error handling (and we also have conveniences to go from Option to Result), which is the desired convention in any case. It would keep the error handling story -- which is already novel -- as simple/concrete as possible. And we can always go more generic later, if desired.

Same with throw/throws.

Second, I agree with @brson's concerns about terminology and keyword choice -- especially given that we have unwinding with panic. Certainly, we should avoid calling this "exception handling". That said, I haven't been able to come up with a compelling alternative to try and catch, and it may be that they're the best choice despite these concerns.

Finally, I'd say that I don't particularly mind the braces for try (to me they help visually delimit the control-flow jump), and prefer having catch to trying to reuse match here (which is pretty ugly).

I'd like to move forward with this RFC soon. Does anyone have concrete proposals for alternative terminology? Does anyone think that Carrier should be committed to up front?

Member

aturon commented Oct 13, 2014

@glaebhoerl

Here are some more thoughts.

First, I think we should skip Carrier and bind directly to Result to start with. This will push people to use Result for error handling (and we also have conveniences to go from Option to Result), which is the desired convention in any case. It would keep the error handling story -- which is already novel -- as simple/concrete as possible. And we can always go more generic later, if desired.

Same with throw/throws.

Second, I agree with @brson's concerns about terminology and keyword choice -- especially given that we have unwinding with panic. Certainly, we should avoid calling this "exception handling". That said, I haven't been able to come up with a compelling alternative to try and catch, and it may be that they're the best choice despite these concerns.

Finally, I'd say that I don't particularly mind the braces for try (to me they help visually delimit the control-flow jump), and prefer having catch to trying to reuse match here (which is pretty ugly).

I'd like to move forward with this RFC soon. Does anyone have concrete proposals for alternative terminology? Does anyone think that Carrier should be committed to up front?

@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

Ericson2314 Oct 14, 2014

Contributor

I forget what Rust's status is on empty enums, absurd patterns, and all that. But a way to make an uninhabited type (let's say Void) so that Result<A, Void> is isomorphic to A would be nice in order to make to make higher-order functions that are both maximally flexible, and don't make function arguments that will never error pay for the extra dispatching on Result.

Edit: I bring it up because I suspect the carrier trait could be used to achieve the same result. Conversely if empty types will suffice, it's one less reason to have a carrier trait.

Contributor

Ericson2314 commented Oct 14, 2014

I forget what Rust's status is on empty enums, absurd patterns, and all that. But a way to make an uninhabited type (let's say Void) so that Result<A, Void> is isomorphic to A would be nice in order to make to make higher-order functions that are both maximally flexible, and don't make function arguments that will never error pay for the extra dispatching on Result.

Edit: I bring it up because I suspect the carrier trait could be used to achieve the same result. Conversely if empty types will suffice, it's one less reason to have a carrier trait.

@nielsle

This comment has been minimized.

Show comment
Hide comment
@nielsle

nielsle Oct 14, 2014

The ? operator looks useful, but I hope that the try! macro survives in one form or the other. The majority of my error handling follows one of the following two patterns, so I would like to avoid specifying catch blocks.

let y = try!(ctx.get_y());
try!(ctx.get_x()?.do_stuff());

If we want the user to think in terms of error propagation then I believe that rust should encourage try!and or_else instead of try {} catch {}. That would be more consistent, and I think that it most use cases (albeit not all )

It might be nice to introduce ?= ... as sugar for = try!( ... ) (But that should probably be discussed elsewhere to avoid derailing this RFC)

nielsle commented Oct 14, 2014

The ? operator looks useful, but I hope that the try! macro survives in one form or the other. The majority of my error handling follows one of the following two patterns, so I would like to avoid specifying catch blocks.

let y = try!(ctx.get_y());
try!(ctx.get_x()?.do_stuff());

If we want the user to think in terms of error propagation then I believe that rust should encourage try!and or_else instead of try {} catch {}. That would be more consistent, and I think that it most use cases (albeit not all )

It might be nice to introduce ?= ... as sugar for = try!( ... ) (But that should probably be discussed elsewhere to avoid derailing this RFC)

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Oct 14, 2014

Contributor

@nikomatsakis I think that's two separate questions, "why is it this way" and "how could it be different". The keywords are there for intuition and convenience (the idea of try without catch actually came much later!); the Carrier trait is there because I think it's important to support Option. But the various features are nearly all separable (just take their "deeply expanded" definitions, without reference to the other features). So yes, just try and ? is also a thing you could do. In that case you could still define some sort of polymorphic unwrap_or_else in place of catch, if you wanted to, I think. (One strange thing here is that try { foo()?.bar() } doesn't entirely mean what it says -- it's always going to "succeed" and return to its caller, just collect any errors from the body. It would actually be better suggestive to write catch { foo()?.bar() }, where you're catching any exceptions that may be thrown. But then it's difficult to explain how that fits into the broader picture of try..catch. This kind of thing may also have been what @wycats was thinking of.)

@aturon I don't mind being incremental, at all, for the reasons you state. (Though I don't think we should be making any distinctions in meaning between Option and Result beyond what is implied by their structure.)

@nielsle Sorry, it's not quite clear to me: what's the difference between ? and try! in your examples? (? as proposed in the RFC is essentially a generalization of the current try!.)

Edit: @Ericson2314: The only reason for Carrier is to be able to support both Option and Result, and it is essentially an implementation detail of the ?, try..catch, etc. constructs. User code need never interact with it directly. (It might be useful for HOFs in some way, but that wasn't the goal.)

Contributor

glaebhoerl commented Oct 14, 2014

@nikomatsakis I think that's two separate questions, "why is it this way" and "how could it be different". The keywords are there for intuition and convenience (the idea of try without catch actually came much later!); the Carrier trait is there because I think it's important to support Option. But the various features are nearly all separable (just take their "deeply expanded" definitions, without reference to the other features). So yes, just try and ? is also a thing you could do. In that case you could still define some sort of polymorphic unwrap_or_else in place of catch, if you wanted to, I think. (One strange thing here is that try { foo()?.bar() } doesn't entirely mean what it says -- it's always going to "succeed" and return to its caller, just collect any errors from the body. It would actually be better suggestive to write catch { foo()?.bar() }, where you're catching any exceptions that may be thrown. But then it's difficult to explain how that fits into the broader picture of try..catch. This kind of thing may also have been what @wycats was thinking of.)

@aturon I don't mind being incremental, at all, for the reasons you state. (Though I don't think we should be making any distinctions in meaning between Option and Result beyond what is implied by their structure.)

@nielsle Sorry, it's not quite clear to me: what's the difference between ? and try! in your examples? (? as proposed in the RFC is essentially a generalization of the current try!.)

Edit: @Ericson2314: The only reason for Carrier is to be able to support both Option and Result, and it is essentially an implementation detail of the ?, try..catch, etc. constructs. User code need never interact with it directly. (It might be useful for HOFs in some way, but that wasn't the goal.)

@mitsuhiko

This comment has been minimized.

Show comment
Hide comment
@mitsuhiko

mitsuhiko Oct 14, 2014

Contributor

How did I miss this RFC? This is awesome. Much better than the comment I left in that other pull request :) I am strong +1 on the concepts in this RFC but I think that the terms try/catch should not be used because this really is not an exception system. Maybe just a variation of the words would indicate that it's something else (catch -> rescue maybe?).

I love that it supports both Option and Result but I wonder how well that works with the different rules about ignoring the return value.

I just want to point to this RFC for defaulted type parameters: #213. This is quite important if you end up in situations where you only care about the error part and the success part is generic. Without that RFC stuff like this would not work:

fn execute<T: FromSomething>(&self) -> Result<FromSomething, Error> {
    FromSomething::value_from_something(...)
}

foo.execute()?

With that RFC you could imply that T might just mean () for instance. I have this problem in my current library where people are forced to write stuff like this:

let _ : () = try!(foo.execute());
Contributor

mitsuhiko commented Oct 14, 2014

How did I miss this RFC? This is awesome. Much better than the comment I left in that other pull request :) I am strong +1 on the concepts in this RFC but I think that the terms try/catch should not be used because this really is not an exception system. Maybe just a variation of the words would indicate that it's something else (catch -> rescue maybe?).

I love that it supports both Option and Result but I wonder how well that works with the different rules about ignoring the return value.

I just want to point to this RFC for defaulted type parameters: #213. This is quite important if you end up in situations where you only care about the error part and the success part is generic. Without that RFC stuff like this would not work:

fn execute<T: FromSomething>(&self) -> Result<FromSomething, Error> {
    FromSomething::value_from_something(...)
}

foo.execute()?

With that RFC you could imply that T might just mean () for instance. I have this problem in my current library where people are forced to write stuff like this:

let _ : () = try!(foo.execute());
@jaredr

This comment has been minimized.

Show comment
Hide comment
@jaredr

jaredr Feb 2, 2016

If people don't mind a drive-by bikeshedding, let me suggest stay { }:

  • You thought you were going to return, but in fact you're going to stay right here
  • It can mean "to steady or support", which is a good match with error handling
  • It can mean "to curb/check/postpone" as in to "stay your hand", which is a decent match with cancelling a hasty return.
  • It verbs well: "the return was stayed by this block", or "the return stays in this block"
  • It's concise

jaredr commented Feb 2, 2016

If people don't mind a drive-by bikeshedding, let me suggest stay { }:

  • You thought you were going to return, but in fact you're going to stay right here
  • It can mean "to steady or support", which is a good match with error handling
  • It can mean "to curb/check/postpone" as in to "stay your hand", which is a decent match with cancelling a hasty return.
  • It verbs well: "the return was stayed by this block", or "the return stays in this block"
  • It's concise
@hauleth

This comment has been minimized.

Show comment
Hide comment
@hauleth

hauleth Feb 2, 2016

@tikue it is exactly what and_then is for:

foo().and_then(Bar::bar).and_then(Baz::baz).map_err(MyErr::from)

or

Ok(try!(foo().and_then(Bar::bar).and_then(Baz::baz)))

But last one doesn't make much sense.

hauleth commented Feb 2, 2016

@tikue it is exactly what and_then is for:

foo().and_then(Bar::bar).and_then(Baz::baz).map_err(MyErr::from)

or

Ok(try!(foo().and_then(Bar::bar).and_then(Baz::baz)))

But last one doesn't make much sense.

@repax

This comment has been minimized.

Show comment
Hide comment
@repax

repax Feb 2, 2016

@jaredr
You can still return from the function and break out of loops containing the proposed construct, so stay might be a little bit confusing as a keyword.

Incidentally, trap and catch might be misleading in the same respect. I.e. return Err(x) is not "caught".

repax commented Feb 2, 2016

@jaredr
You can still return from the function and break out of loops containing the proposed construct, so stay might be a little bit confusing as a keyword.

Incidentally, trap and catch might be misleading in the same respect. I.e. return Err(x) is not "caught".

@tikue

This comment has been minimized.

Show comment
Hide comment
@tikue

tikue Feb 2, 2016

@hauleth it's not the same; and_then requires you manually unify error types.

tikue commented Feb 2, 2016

@hauleth it's not the same; and_then requires you manually unify error types.

@burdges

This comment has been minimized.

Show comment
Hide comment
@burdges

burdges Feb 3, 2016

@hauleth We've a much more efficient implementation for Result though, or heck even for a state monad, so the situations must be distinguished somehow.

@rpjohnst Idris' effects types address similar issues, like the annoyance of monad transformers not being commutative, by being more restrictive than monads. I'd imagined it covered all the monads we're trying to avoid, but actually maybe it's close to the ones we want. It's another beast about which I know relatively little though.

I suppose a useful question is : Is there anything wrong with adding a state type to Carrier so that stored types become (State,Normal) and Exception, or maybe (State,Exception), and then ? helps the functions handle the state? I worry about this example because adding state seems like just sugar in Rust, but it's the example that comes to mind.

burdges commented Feb 3, 2016

@hauleth We've a much more efficient implementation for Result though, or heck even for a state monad, so the situations must be distinguished somehow.

@rpjohnst Idris' effects types address similar issues, like the annoyance of monad transformers not being commutative, by being more restrictive than monads. I'd imagined it covered all the monads we're trying to avoid, but actually maybe it's close to the ones we want. It's another beast about which I know relatively little though.

I suppose a useful question is : Is there anything wrong with adding a state type to Carrier so that stored types become (State,Normal) and Exception, or maybe (State,Exception), and then ? helps the functions handle the state? I worry about this example because adding state seems like just sugar in Rust, but it's the example that comes to mind.

@suhr

This comment has been minimized.

Show comment
Hide comment
@suhr

suhr Feb 3, 2016

Maybe @edwinb or @david-christiansen will tell more about Idris effect system.

suhr commented Feb 3, 2016

Maybe @edwinb or @david-christiansen will tell more about Idris effect system.

@rpjohnst

This comment has been minimized.

Show comment
Hide comment
@rpjohnst

rpjohnst Feb 3, 2016

@burdges Ah, so it's a way to make the desugaring work in more situations? Nifty, but I would think that as far as Rust goes we would want to keep control flow the same inside, outside, and across boundaries, rather than desugaring it in some cases.

rpjohnst commented Feb 3, 2016

@burdges Ah, so it's a way to make the desugaring work in more situations? Nifty, but I would think that as far as Rust goes we would want to keep control flow the same inside, outside, and across boundaries, rather than desugaring it in some cases.

@stevenblenkinsop

This comment has been minimized.

Show comment
Hide comment
@stevenblenkinsop

stevenblenkinsop Feb 3, 2016

@rpjohnst - Ideally, you wouldn't be able to tell the difference, and the generated code would be the same either way. I'm not familiar enough with Idris to know whether this is possible in their model, though. But I agree, the behaviour of the construct shouldn't change depending on whether support for a given type is implemented in the compiler or in a library. That's why I'm skeptical of claims that the behaviour of this construct could be unified with a generalized monadic construct as long as they don't have a detailed design backing them up.

stevenblenkinsop commented Feb 3, 2016

@rpjohnst - Ideally, you wouldn't be able to tell the difference, and the generated code would be the same either way. I'm not familiar enough with Idris to know whether this is possible in their model, though. But I agree, the behaviour of the construct shouldn't change depending on whether support for a given type is implemented in the compiler or in a library. That's why I'm skeptical of claims that the behaviour of this construct could be unified with a generalized monadic construct as long as they don't have a detailed design backing them up.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 5, 2016

Contributor

Huzzah! The @rust-lang/lang subteam has decided to accept this RFC (with a few tweaks described below). Thanks everyone how participated in the rather, ahem, extended discussion (more than a year!). If you're interested in seeing a summary, we attempted to summarize the discussion in two previous comments that are still mostly complete.

Given the amount of discussion on this thread, I just want to reiterate that accepting the RFC is only the first step in the overall stability process. The next step is to open a tracking issue and begin work on implementation. Further discussion about this feature can be had on the tracking issue. Once we are satisfied with the state of the implementation -- and in particular we feel we have resolved the various unresolved questions -- we will start a second FCP period on the tracking issue itself. This lasts for one release cycle (6 weeks). We will then make a final decision about ungating. In the case of this RFC, there are two distinct feature-gates, one for the ? operator and one for the "try-catch" functionality. Most likely those will be stabilized at different times (and of course we can always refine further, creating new feature-gate for subsets of the functionality if necessary).

Some specific issues we plan to revisit prior to stabilizing:

  • whether to extend to other types beyond Result;
  • compatibility (or lack thereof) with a more general do notation (and desirability thereof).

Now, on to the tweaks. We decided that for now we will simplify the RFC: rather than supporting the try/catch construct currently described, we'll just support a catch { ... } expression. catch will intercept any ? expressions within its body, just as try used to do. To get the equivalent of try { ... } catch { ... } functionality, then, one would do catch { ... }.unwrap_or(|v| match v { ... }).

Our reasoning was as follows:

  • This variation has been proposed at various times on the RFC thread, most recently by the RFC author.
  • Although try in many ways feels like the correct choice, the drawbacks are real:
    • opposite meaning of the try! macro -- even if we were to deprecate try!, it will be some time before we can do so (? must be stable first), and in that time try! will remain the recommended pattern for stable crates; there is already a large body of code, blog posts, documentation, etc that refers to try!, and that will only get larger. So even once we deprecate it, it will be even longer until all of those references are updated (if theythey ever are). Moreover, in the interim, both try! and try will co-exist on nightly, which could be very confusing, given that they do very different things.
    • the lingering concerns that try { } catch { } suggests automatic unwinding where none is happening
  • The catch (originally: try) keyword adds the real expressive "step up" here, the match (originally: catch) was just sugar for unwrap_or.
  • It would be easy to add further sugar in the future, once we see how catch is used (or not used) in practice.
  • There was some concern about potential user confusion about two aspects:
    • catch { } yields a Result<T,E> but catch { } match { } yields just T;
    • catch { } match { } handles all kinds of errors, unlike try/catch in other languages which let you pick and choose.

All right people, show's over, nothing to see here, move along (to the tracking issue). :)

Contributor

nikomatsakis commented Feb 5, 2016

Huzzah! The @rust-lang/lang subteam has decided to accept this RFC (with a few tweaks described below). Thanks everyone how participated in the rather, ahem, extended discussion (more than a year!). If you're interested in seeing a summary, we attempted to summarize the discussion in two previous comments that are still mostly complete.

Given the amount of discussion on this thread, I just want to reiterate that accepting the RFC is only the first step in the overall stability process. The next step is to open a tracking issue and begin work on implementation. Further discussion about this feature can be had on the tracking issue. Once we are satisfied with the state of the implementation -- and in particular we feel we have resolved the various unresolved questions -- we will start a second FCP period on the tracking issue itself. This lasts for one release cycle (6 weeks). We will then make a final decision about ungating. In the case of this RFC, there are two distinct feature-gates, one for the ? operator and one for the "try-catch" functionality. Most likely those will be stabilized at different times (and of course we can always refine further, creating new feature-gate for subsets of the functionality if necessary).

Some specific issues we plan to revisit prior to stabilizing:

  • whether to extend to other types beyond Result;
  • compatibility (or lack thereof) with a more general do notation (and desirability thereof).

Now, on to the tweaks. We decided that for now we will simplify the RFC: rather than supporting the try/catch construct currently described, we'll just support a catch { ... } expression. catch will intercept any ? expressions within its body, just as try used to do. To get the equivalent of try { ... } catch { ... } functionality, then, one would do catch { ... }.unwrap_or(|v| match v { ... }).

Our reasoning was as follows:

  • This variation has been proposed at various times on the RFC thread, most recently by the RFC author.
  • Although try in many ways feels like the correct choice, the drawbacks are real:
    • opposite meaning of the try! macro -- even if we were to deprecate try!, it will be some time before we can do so (? must be stable first), and in that time try! will remain the recommended pattern for stable crates; there is already a large body of code, blog posts, documentation, etc that refers to try!, and that will only get larger. So even once we deprecate it, it will be even longer until all of those references are updated (if theythey ever are). Moreover, in the interim, both try! and try will co-exist on nightly, which could be very confusing, given that they do very different things.
    • the lingering concerns that try { } catch { } suggests automatic unwinding where none is happening
  • The catch (originally: try) keyword adds the real expressive "step up" here, the match (originally: catch) was just sugar for unwrap_or.
  • It would be easy to add further sugar in the future, once we see how catch is used (or not used) in practice.
  • There was some concern about potential user confusion about two aspects:
    • catch { } yields a Result<T,E> but catch { } match { } yields just T;
    • catch { } match { } handles all kinds of errors, unlike try/catch in other languages which let you pick and choose.

All right people, show's over, nothing to see here, move along (to the tracking issue). :)

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Feb 5, 2016

Member

catch { ... }.unwrap_or(|v| match v { ... })

match catch { ... } { Ok(x) => x, Err(...) => ..., ... } might look better in some cases, fwiw.

Member

eddyb commented Feb 5, 2016

catch { ... }.unwrap_or(|v| match v { ... })

match catch { ... } { Ok(x) => x, Err(...) => ..., ... } might look better in some cases, fwiw.

@repax

This comment has been minimized.

Show comment
Hide comment
@repax

repax Feb 5, 2016

Or like this:

if let Err(e) = catch {
    ...
} {
    handle(e);
}

repax commented Feb 5, 2016

Or like this:

if let Err(e) = catch {
    ...
} {
    handle(e);
}

nikomatsakis added a commit to nikomatsakis/rfcs that referenced this pull request Feb 5, 2016

@logannc

This comment has been minimized.

Show comment
Hide comment
@logannc

logannc Feb 5, 2016

Well, isn't that nifty. If the catch block is big that might look odd
having ... } { ... but its sort of nice that there is nothing else
required.
On Feb 5, 2016 1:43 PM, "Eduard-Mihai Burtescu" notifications@github.com
wrote:

catch { ... }.unwrap_or(|v| match v { ... })

match catch { ... } { Ok(x) => x, Err(...) => ..., ... } might look
better in some cases, fwiw.


Reply to this email directly or view it on GitHub
#243 (comment).

logannc commented Feb 5, 2016

Well, isn't that nifty. If the catch block is big that might look odd
having ... } { ... but its sort of nice that there is nothing else
required.
On Feb 5, 2016 1:43 PM, "Eduard-Mihai Burtescu" notifications@github.com
wrote:

catch { ... }.unwrap_or(|v| match v { ... })

match catch { ... } { Ok(x) => x, Err(...) => ..., ... } might look
better in some cases, fwiw.


Reply to this email directly or view it on GitHub
#243 (comment).

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Feb 5, 2016

Contributor

@nikomatsakis So where's the tracking issue? :)

@eddyb @logannc Wouldn't that run afoul of the same parser shenanigans which prevent using struct literals post-match? I recall some discussion upthread to that effect. I assume you could solve it by putting parentheses around the catch { }...

Contributor

glaebhoerl commented Feb 5, 2016

@nikomatsakis So where's the tracking issue? :)

@eddyb @logannc Wouldn't that run afoul of the same parser shenanigans which prevent using struct literals post-match? I recall some discussion upthread to that effect. I assume you could solve it by putting parentheses around the catch { }...

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 5, 2016

Contributor

@glaebhoerl sorry, still catching up on the "paperwork" side here :) will open the tracking issue in a second.

Contributor

nikomatsakis commented Feb 5, 2016

@glaebhoerl sorry, still catching up on the "paperwork" side here :) will open the tracking issue in a second.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 5, 2016

Contributor

Regarding catch as a match discriminant, I think it will run afoul of the rules regarding match expressions without parentheses, though the if let form would work fine (but of course is slightly different in its effect).

(I am presuming we are going to add catch as a contextual keyword; if it were a keyword from the start, we could potentially allow it as a match argument I guess.)

Contributor

nikomatsakis commented Feb 5, 2016

Regarding catch as a match discriminant, I think it will run afoul of the rules regarding match expressions without parentheses, though the if let form would work fine (but of course is slightly different in its effect).

(I am presuming we are going to add catch as a contextual keyword; if it were a keyword from the start, we could potentially allow it as a match argument I guess.)

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 5, 2016

Contributor

Tracking issue: rust-lang/rust#31436

Contributor

nikomatsakis commented Feb 5, 2016

Tracking issue: rust-lang/rust#31436

@nikomatsakis nikomatsakis merged commit 2daab80 into rust-lang:master Feb 5, 2016

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 5, 2016

Contributor

OK, merged (with some edits). If anybody sees any place that I missed a reference to try/catch or something like that, let me know.

Contributor

nikomatsakis commented Feb 5, 2016

OK, merged (with some edits). If anybody sees any place that I missed a reference to try/catch or something like that, let me know.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 5, 2016

Contributor

See in particular: 94390a2

Contributor

nikomatsakis commented Feb 5, 2016

See in particular: 94390a2

@aturon aturon changed the title from Trait-based exception handling to First-class error handling with `?` and `catch` Feb 5, 2016

@durka

This comment has been minimized.

Show comment
Hide comment
@durka

durka Feb 5, 2016

Contributor

@nikomatsakis search for "try block" to find a bunch of missed replacements. And the "Laws" seem like they'll need to be rewritten.

Contributor

durka commented Feb 5, 2016

@nikomatsakis search for "try block" to find a bunch of missed replacements. And the "Laws" seem like they'll need to be rewritten.

@nrc

This comment has been minimized.

Show comment
Hide comment
@nrc

nrc May 18, 2016

Member

PR 33389 adds experimental support for the Carrier trait. Since it wasn't part of the original RFC, it should get a particularly close period of examination and discussion before we move to FCP (which should probably be separate to the FCP for the rest of the ? operator). If the trait is still contentious after experimentation and discussion, then we can open an RFC (the language team felt this did not need to be the default path though). See this discuss thread for more details.

Member

nrc commented May 18, 2016

PR 33389 adds experimental support for the Carrier trait. Since it wasn't part of the original RFC, it should get a particularly close period of examination and discussion before we move to FCP (which should probably be separate to the FCP for the rest of the ? operator). If the trait is still contentious after experimentation and discussion, then we can open an RFC (the language team felt this did not need to be the default path though). See this discuss thread for more details.

@bugaevc bugaevc referenced this pull request May 29, 2016

Closed

Async IO #1081

@MatejLach MatejLach referenced this pull request Aug 7, 2016

Merged

Change decoding functions #3

@sciyoshi

This comment has been minimized.

Show comment
Hide comment
@sciyoshi

sciyoshi Sep 10, 2016

I didn't see any discussion in this thread about the possibility of reserving ? as syntax for nullable types, i.e. Option. I feel like Option (and Result) are common enough that they might eventually warrant shortened syntax, like u32? rather than Option<u32>. Would that still be possible with this RFC?

sciyoshi commented Sep 10, 2016

I didn't see any discussion in this thread about the possibility of reserving ? as syntax for nullable types, i.e. Option. I feel like Option (and Result) are common enough that they might eventually warrant shortened syntax, like u32? rather than Option<u32>. Would that still be possible with this RFC?

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Sep 11, 2016

Contributor

@sciyoshi Yes, types and values have separate syntax.

Contributor

glaebhoerl commented Sep 11, 2016

@sciyoshi Yes, types and values have separate syntax.

@est31 est31 referenced this pull request Sep 16, 2016

Closed

Longer question marks RFC #1737

withoutboats pushed a commit to withoutboats/rfcs that referenced this pull request Jan 15, 2017

Merge pull request #243 from alexbool/master
futures-cpupool: enrich `Builder` with thread name prefix
@KalitaAlexey

This comment has been minimized.

Show comment
Hide comment
@KalitaAlexey

KalitaAlexey Feb 27, 2017

What is going to be implemented for the feature?
? is implemented. catch is being implemented. How about match?

KalitaAlexey commented Feb 27, 2017

What is going to be implemented for the feature?
? is implemented. catch is being implemented. How about match?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment