Skip to content
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

Conversation

glaebhoerl
Copy link
Contributor

@glaebhoerl 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
Copy link

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
Copy link
Member

aturon commented Sep 16, 2014

@netvl That's covered by an earlier RFC for error interoperation.

@Ericson2314
Copy link
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.


try {
foo()?.bar()?
} catch e {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
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
Copy link
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)

@nikomatsakis
Copy link
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.

@aturon
Copy link
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
Copy link
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".

@arcto
Copy link

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
Copy link
Contributor Author

@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.)

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`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@hatahet
Copy link

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
Copy link
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
Copy link

suhr commented Oct 8, 2014

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

@nikomatsakis
Copy link
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.

@arthurprs
Copy link

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
Copy link
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
Copy link

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
Copy link
Contributor Author

@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
Copy link
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.

@nikomatsakis
Copy link
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.)

@nikomatsakis
Copy link
Contributor

Tracking issue: rust-lang/rust#31436

@nikomatsakis nikomatsakis merged commit 2daab80 into rust-lang:master Feb 5, 2016
@nikomatsakis
Copy link
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.

@nikomatsakis
Copy link
Contributor

See in particular: 94390a2

@aturon aturon changed the title Trait-based exception handling First-class error handling with ? and catch Feb 5, 2016
@durka
Copy link
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
Copy link
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.

@sciyoshi
Copy link

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
Copy link
Contributor Author

@sciyoshi Yes, types and values have separate syntax.

@est31 est31 mentioned this pull request Sep 16, 2016
withoutboats pushed a commit to withoutboats/rfcs that referenced this pull request Jan 15, 2017
futures-cpupool: enrich `Builder` with thread name prefix
@KalitaAlexey
Copy link

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

@coolCucumber-cat
Copy link

Why does the try block wrap the return value in whatever the successful wrapper value is, for example, Ok for Result or Some for Option? It's very confusing because functions returning those types don't do it. It's very confusing, especially with wrapped types or if you just want to return the value the normal way. In a situation where you could just return a Result from a function, for a try block, you have to add a "?" to the end for no reason.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-error-handling Proposals relating to error handling. A-expressions Term language related proposals & ideas A-syntax Syntax related proposals & ideas A-traits-libstd Standard library trait related proposals & ideas final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.