Rust: Tracking issue for `?` operator and `try` blocks (RFC 243, `question_mark` & `try_blocks` features)

Created on 5 Feb 2016  ·  340Comments  ·  Source: rust-lang/rust

Tracking issue for rust-lang/rfcs#243 and rust-lang/rfcs#1859.

Implementation concerns:

  • [x] ? operator that is roughly equivalent to try! - #31954
  • [x] try { ... } expression - https://github.com/rust-lang/rust/issues/39849

    • [x] resolve do catch { ... } syntax question


    • [x] resolve whether catch blocks should "wrap" result value (first addressed in https://github.com/rust-lang/rust/issues/41414, now being settled anew in https://github.com/rust-lang/rust/issues/70941)

    • [ ] Address issues with type inference (try { expr? }? currently requires an explicit type annotation somewhere).

  • [x] settle design of the Try trait (https://github.com/rust-lang/rfcs/pull/1859)

    • [x] implement new Try trait (in place of Carrier) and convert ? to use it (https://github.com/rust-lang/rust/pull/42275)

    • [x] add impls for Option and so forth, and a suitable family of tests (https://github.com/rust-lang/rust/pull/42526)

    • [x] improve error messages as described in the RFC (https://github.com/rust-lang/rust/issues/35946)

  • [x] reserve try in new edition
  • [x] block try{}catch (or other following idents) to leave design space open for the future, and point people to how to do what they want with match instead
A-error-handling B-RFC-approved B-RFC-implemented B-unstable C-tracking-issue F-try_blocks Libs-Tracked T-lang T-libs

Most helpful comment

@mark-i-m I don't think we can reasonably move from one to the other after stabilization. As bad as I consider Ok-wrapping to be, inconsistent Ok-wrapping that tries to guess whether you want it or not would be even worse.

All 340 comments

The accompanying RFC discusses a desugaring based on labeled return/break, are we getting that too or will there just be special treatment for ? and catch in the compiler?

EDIT: I think labeled return/break is an excellent idea separate from ? and catch, so if the answer is no I will probably open a separate RFC for it.

Labeled return/break is purely for explanatory purposes.

On Fri, Feb 5, 2016 at 3:56 PM, Jonathan Reem [email protected]
wrote:

The accompanying RFC discusses a desugaring based on labeled return/break,
are we getting that too or will there just be special treatment for ? and
catch in the compiler?


Reply to this email directly or view it on GitHub
https://github.com/rust-lang/rust/issues/31436#issuecomment-180551605.

Another unresolved question we have to resolve before stabilizing is what the contract which impls of Into have to obey should be -- or whether Into is even the right trait to use for the error-upcasting here. (Perhaps this should be another checklist item?)

@reem

I think labeled return/break is an excellent idea ... I will probably open a separate RFC for it.

Please do!

On the subject of the Carrier trait, here is a gist example of such a trait I wrote back early in the RFC process.
https://gist.github.com/thepowersgang/f0de63db1746114266d3

How this is treated during parsing?

struct catch {
    a: u8
}

fn main() {

    let b = 10;
    catch { a: b } // struct literal or catch expression with type ascription inside?

}

@petrochenkov Well, the definition couldn't affect parsing, but I think we still have a lookahead rule, based on the second token after {, : in this case, so it should still be parsed as a struct literal.

Also

let c1 = catch { a: 10 };
let c2 = catch { ..c1 }; // <--

struct catch {}
let c3 = catch {}; // <--

+ https://github.com/rust-lang/rfcs/issues/306 if (when!) implemented.
It seems like there are no conflicts besides struct literals.

Given the examples above I'm for the simplest solution (as usual) - always treat catch { in expression positions as start of a catch block. Nobody calls their structures catch anyway.

It would be easier if a keyword was used instead of catch.

@bluss yeah, I admit none of them are great... override seems like the only one that is close. Or we could use do, heh. Or a combination, though I don't see any great ones immediately. do catch?

do is the only one that seems to be close IMO. A keyword soup with do as prefix is a bit irregular, not similar to any other part of the language? Is while let a keyword soup as well? That one feels ok now, when you are used to it.

port try! to use ?

Can't ? be ported to use try! instead? This would allow for the use case where you want to get a Result return path, e.g. when debugging. With try! this is fairly easy, you just override the macro at the beginning of the file (or in lib/main.rs):

macro_rules! try {
    ($expr:expr) => (match $expr {
        Result::Ok(val) => val,
        Result::Err(err) => {
            panic!("Error occured: {:?}", err)
        }
    })
}

You will get a panic stack trace starting from the first occurrence of try! in the Result return path. In fact, if you do try!(Err(sth)) if you discover an error instead of return Err(sth), you even get the full stack trace.

But when debugging foreign libraries written by people who haven't implemented that trick, one relies on try! usage somewhere higher in the chain. And now, if the library uses the ? operator with hardcoded behaviour, getting a stacktrace gets almost impossible.

It would be cool if overriding try! would affect the ? operator as well.

Later on when the macro system gets more features you can even panic! only for specific types.

If this proposal requires an RFC please let me know.

Ideally ? could just be extended to provide stack trace support directly, rather than relying on the ability to override try!. Then it would work everywhere.

Stack traces are just one example (though a very useful one, seems to me).
If the Carrier trait is made to work, maybe that can cover such extensions?

On Sun, Feb 7, 2016 at 4:14 PM, Russell Johnston [email protected]
wrote:

Ideally ? could just be extended to provide stack trace support directly,
rather than relying on the ability to override try!. Then it would work
everywhere.


Reply to this email directly or view it on GitHub
https://github.com/rust-lang/rust/issues/31436#issuecomment-181118499.

Without wanting to speculate, I think that it could work, albeit with some issues.

Consider the usual case where one has code returning some Result<V,E> value. Now we would need to allow for multiple implementations of the Carrier trait to coexist. In order to not run into E0119, one has to make all implementations out of scope (possibly through different traits which are per default not imported), and when using the ? operator, the user is required to import the wished implementation.

This would require everybody, even those who don't want to debug, to import their wished trait implementation when using ?, there would be no option for a predefined default.

Possibly E0117 can be an issue too if wanting to do custom Carrier implementations for Result<V,E>, where all types are outside bounds, so libstd should provide already provide a set of implementations of the Carrier trait with the most used use cases (trivial implementation, and panic! ing implementation, perhaps more).

Having the possibility to override via a macro would provide a greater flexibility without the additional burden on the original implementor (they don't have to import their wished implementation). But I also see that rust never had a macro based operator before, and that implementing ? via a macro isn't possible if catch { ... } is supposed to work, at least not without additional language items (return_to_catch, throw, labeled break with param as used in RFC 243).

I am okay with any setup which enables one to get Result stacktraces with an Err return path, while having only to modify a very small amount of code, prefferably at the top of the file. The solution should also work unrelated to how and where the Err type is implemented.

Just to chime in on bikeshedding: catch in { ... } flows pretty nicely.

catch! { ... } is another backcompat choice.

Also, not that I expect this to change anything, but a note that this is going to break multi-arm macros that were accepting $i:ident ?, in the same way that type ascription broke $i:ident : $t:ty.

Don't overdo the backwards compatibility, just treat catch as a keyword when followed by { (possibly only in expression position, but I'm not sure if that changes much compatibility-wise).

I can also imagine some possible problems that don't involve struct literals (e.g. let catch = true; if catch {}); but I prefer a minor breaking change over a more ugly syntax.

Didn't we have a for adding new keywords, anyways? We could offer some kind of from __future__ opt-in for new syntax; or specify a rust language version number on the command-line / in Cargo.toml.
I highly doubt that in the long term, we'll be able to work with only those keywords that are already reserved. We don't want our keywords to have three different meanings each, depending on context.

I agree. This isn't even the first RFC where this has come up (https://github.com/rust-lang/rfcs/pull/1444 is another example). I expect it won't be the last. (Also default from https://github.com/rust-lang/rfcs/pull/1210, although it's not an RFC I'm in favor of.) I think we need to find a way to add honest-to-god keywords instead of trying to figure out how to ad-hoc hack the grammar for every new case.

Wasn't the whole argument for not reserving several keywords prior to 1.0 that we'd definitely be introducing a way to add new keywords to the language backward compatibly (possibly by explicitly opting in), so there was no point? Seems like now would be a good time.

@japaric Are you interested in reviving your old PR and taking this on?

@aturon My implementation simply desugared foo? in the same way as try!(foo). It also only worked on method and function calls, i.e. foo.bar()? and baz()? work but quux? and (quux)? don't. Would that be okay for an initial implementation?

@japaric What was the reason for restricting it to methods and function calls? Wouldn't parsing it be easier as a general postfix operator?

What was the reason for restricting it to methods and function calls?

easiest way (for me) to test the ? expansion

Wouldn't parsing it be easier as a general postfix operator?

probably

@japaric It'd probably be good to generalize it to a full postfix operator (as @eddyb is suggesting), but it's fine to land ? with the simple desugaring and then add catch later.

@aturon Alright, I'll look into the postfix version by next week if no one beats me to it :-).

rebased/updated my PR in #31954 :-)

What about support for providing stack traces? Is that planned?

I hate to be the +1 guy, but stack traces have saved good chunks of my time in the past. Maybe on debug builds, and when hitting the error path, the ? operator could append the file/line to a Vec in Result? Maybe the Vec could be debug-only too?

And that could be either exposed or turned into part of the error description...

I keep running into the situation where I want to use try! / ? inside iterators returning Option<Result<T, E>>. Unfortunately that currently does not really work. I wonder if the carrier trait could be overloaded to support this or would that go into a more generic From instead?

@hexsel I really wish the Result<> type would carry a vec of instruction pointers in debug and ? would append to it. That way DWARF info could be used to build a readable stacktrace.

@mitsuhiko But how could you create and pattern-match Result? It's _just_ an enum atm.

As for the Option wrapping, I believe you want Some(catch {...}).

Currently, my habit right now is to do try!(Err(bla)) instead of return Err(), so that I can override the try macro later on with one that panics, in order to get a backtrace. It works well for me, but the code I deal with is very low level, and mostly originates the errors. I still will have to avoid ? if I use external code that returns Result.

@eddyb it would need language support to carry hidden values in addition that you need to manipulate by other means. I was wondering if it can be done in other ways but I can't see how. The only other way would have been a standardized error box that can have additional data on it, but there is no requirement to have boxed errors and most people don't do it.

@mitsuhiko I can think of a new (default) method on the Error trait and TLS.
The latter is used by sanitizers sometimes.

@eddyb that only works though if the Result can be identified and that requires it to be boxed or it will move around in memory as it passes upwards the stack.

@mitsuhiko The TLS? Not really, you just need to be able to compare the error by-value.

Or even just by type (with linking From inputs and outputs), how many concurrent errors you want stacktraces from will ever have to be propagated simultaneously?

I am against adding Result-specific compiler hacks, personally, when simpler solutions work.

@eddyb the error passes upwards the stack. What you want is the EIP at every stack level, not just where it originates. Also errors are a) currently not comparable and b) just because they compare equal does not mean they are the same error.

how many concurrent errors you want stacktraces from will ever have to be propagated simultaneously

Any error caught down and rethrown as different error.

I am against adding Result-specific compiler hacks, personally, when simpler solutions work.

I don't see how a simpler solution works but maybe I'm missing something there.

You can save the instruction pointer at every ? and correlate it with the error type.
"Any error caught down and rethrown as different error." But how would you preserve that information if it was hidden in Result?

But how would you preserve that information if it was hidden in Result?

You don't need to store that information in the result. What you do need to store though is a unique ID for the origin of the failure so you can correlate it. And because the error trait is just a trait and does not have storage, it could be stored in the result instead. The instruction pointer vec itself would by no means have to be stored in result, that could go to TLS.

One way would be that you invoke a method failure_id(&self) on the result and it returns an i64/uuid or something that identifies the origin of the failure.

This would need language support no matter what because what you need is that as the result passes upwards through the stack, the compiler injects an instruction to record the stack frame it falls through. So the return of a result would look different in debug builds.

"the compiler injects an instruction to record the stack frame it falls through" - but ? is explicit, this is nothing like exceptions, or do you not like recording _only_ the ? it passed through?

Anyway, if you manually unpack the error and then put it back in an Err, how would that ID even be kept?

"And because the error trait is just a trait and does not have storage, it could be stored in the result instead"
There is a variant on this: implementing the Error trait could be special-cased in the compiler to add an extra integer field to the type, creating the type would trigger an ID to be generated, and copy/drop would effectively increment/decrement the refcount (and eventually clear it from the TLS "in-flight error set" if Result::unwrap is not used).

But that would conflict with the Copy trait. I mean, so would your plan, adding any special behavior to Result that is _not_ triggered by ? or other specific user actions can invalidate the Copy invariants.

EDIT: At this point you might as well embed an Rc<ErrorTrace> in there.
EDIT2: What am I even saying, you can clear the associated error trace on catch.
EDIT3: Actually, on drop, see below a better explanation.

"the compiler injects an instruction to record the stack frame it falls through" - but ? is explicit, this is nothing like exceptions, or do you not like recording only the ? it passed through?

That does not work because there are too many frames you can fall through which do not use ?. Let alone that not everybody is going to handle errors with just ?.

Anyway, if you manually unpack the error and then put it back in an Err, how would that ID be even kept?

That's why it would have to be compiler support. The compiler would have to track local variables that are results and do it's best to propagate the result id onwards for re-wraps. If this is too magical then it could be restricted to a subset of operations.

That does not work because there are too many frames you can fall through which do not use ?. Let alone that not everybody is going to handle errors with just ?.

Okay, I could see returning Result directly be special-cased in complex functions with multiple return paths (some of which would be early returns from ?).

If this is too magical then it could be restricted to a subset of operations.

Or made entirely explicit. Do you have examples of non-? re-wrapping that would have to be magically tracked by the compiler?

@eddyb The common case of manual handling of errors is an IoError where you want to handle a subset:

loop {
  match establish_connection() {
    Ok(conn) => { ... },
    Err(err) => {
      if err.kind() == io::ErrorKind::ConnectionRefused {
        continue;
      } else {
        return Err(err);
      }
    }
  }
}

@mitsuhiko Then keeping the ID inside io::Error would definitely work.

So to recapitulate, a Vec<Option<Trace>> "sparse integer map" in TLS, keyed by struct ErrorId(usize) and accessed by 3 lang items:

  • fn trace_new(Location) -> ErrorId, when creating a non-const error value
  • fn trace_return(ErrorId, Location), just before returning from a function _declared_ as -> Result<...> (i.e. not a function generic on the return that that _happens_ to be used with a Result type there)
  • fn trace_destroy(ErrorId), when dropping an error value

If the transformation is done on MIR, Location can be computed from the Span of the instruction triggering either construction of an error value or writing to Lvalue::Return, which is much more reliable than an instruction pointer IMO (no easy way to get that in LLVM anyway, you'd have to emit inline asm for each specific platform).

@eddyb

Won't that lead to binary-size bloat?

@arielb1 You'd do it in debug mode only where the debuginfo bloats the binary anyway - you could also reuse the debuginfo cleverly _shrug_.

@eddyb what is a Location in that case? Not sure what's hard about reading the IP is. Sure, you need custom JS for each target but that's not really all that hard.

@mitsuhiko it could be the same info we use for overflow checks and other compiler-emitted panics. See core::panicking::panic.

Why pack so much stack information to an Error/Result while the stack is unwound? Why not just run the code while the stack is still there? E.g. what if you are interested in variables on the path of the stack? Just enable people to run custom code when an Err gets invoked, e.g. to call up a debugger of their choice. This is what try! already provides due to being an (overrideable) macro. The easiest way is to panic on an Err case which prints the stack provided one has started the program with the right params.

With try you can do anything you want on an Err case, and you can override the macro crate wide, or very scoped to not touch performance critical code e.g. if the error is hard to reproduce and you need to run alot of the perf critical code.

Nobody would need to preserve a fraction of the information in an artificial stack one builds up because the real one is going to be destroyed. All the macro overriding method could be improved with is:

  • ? being overridable in a similar way, easiest way would be by defining ? as sugar for try! -- especially needed in order to catch non originating errors at the crate border as outlined in my comment above.
  • having a more powerful macro system, like matching on the type in order to allow for even more flexibility. Yes, one could think about putting this into the traditional trait system, but rust allows no overriding of implementations of traits, so that's going to be a bit complicated

@est31 The stack is not _automatically_ "unwound" like in a panic, this is syntactical sugar for early returns.
And yes, you could have the compiler insert calls of some empty functions with fixed names that you can breakpoint on, this is pretty easy (and also have information that lets you do, e.g. call dump(data) - where dump and data are arguments debuggers can see).

@eddyb I think empty functions would not allow a use-case of, e.g. keeping a few "canary" debug instances on a large deployment to see if errors appear in logs, to then go back and fix things. I understand it is preferrable to be proactive than reactive, but not everything is easy to predict

@hexsel Yeah, which is why I prefer the TLS-based method where Result::unwrap or some new ignore method (or even always when dropping the error?) dumps the trace to stderr.

@eddyb If you add the instruction pointer or something derived from the value to a stack like data structure in TLS then you basically rebuild your small version of the real life stack. A return reduces the stack by one entry. So if you do that while you return you _unwind_ part of the stack, while building a limited version of it at some other place in RAM. Perhaps "unwind" is the wrong term for behaviour resulting from "legal" returns, but if all code does ? or try! and at the end you intercept the end result is the same. Its great that rust does not make error propagation automatic, I really liked how java had required all exceptions to be listed after the throws keyword, rust is a good improvement on that.

@hexsel an overriding (as it already exists now for try!) based approach would allow for this -- you can run any code you wish, and log to any logging system. You would need some detection for "dupes" when multiple try's catch the same error as it propagates up the stack though.

@est31 Overriding try! only works in your own code (it's literally macro import shadowing), it would have to be something different, like our "weak lang items".

@est31 That's not really correct (about unwinding), the traces and the real stack don't necessarily have any relationship, because moving Results around doesn't have to go up in the original backtrace, it can go sideways too.
Also, if the binary is optimized, most of the backtrace is gone, and if you don't have debug info, then Location is strictly superior. You could even be running an obfuscating plugin which replaces all the source information with random hashes that can be matched by the developers of some closed source product.

Debuggers are useful (and as an aside, I'm loving lldb's cleaner backtrace output), but they're not a panacea, and we already output some information on panics so you can get some clues as to what's going on.

About that - I had some thoughts about a typesystem-level trick where {Option,Result}::unwrap would have an extra type argument, defaulting to a type dependent on the location the function was called from, such that the panics from those would have way more useful location information.

With progress on value parametrization, that might still be an option, but definitely not the only one, and I don't want to outright dismiss Result traces, instead I'm trying to find a model which is _implementable_.

Overriding try! is not a solution at all because it's contained within your own crate. That's unacceptable as debugging experience. I already tried plenty of that trying to deal with the current try!. Especially if you have many crates involved and errors get transmuted multiple times on the way through the stack it's nearly impossible to figure out where the error originates and why. If a bandaid like that is the solution then we will have to revisit error handling in general for large Rust projects.

@eddyb so is your suggestion that we literally embed the filename, function-name, line number and column number to that vector? That seems hugely wasteful particularly because that information is already contained in much more processable format in DWARF. Also DWARF lets us use the same process reasonably cheaply in production whereas this sort of location info appears to be so wasteful that nobody would ever run a release binary with it.

How would it be significantly more wasteful than DWARF information? Filenames would be deduplicated, and on x64 the whole thing is the size of 3 pointers.

@mitsuhiko so basically, you agree with the general direction, but not with the technical specifics of it?

How easy is it to expose DWARF information to a general Rust API?

@eddyb because DWARF information is not contained within the release binary but separate files. So I can have the debug files on symbols servers and ship a stripped binary to users.

@mitsuhiko Oh, that's an entirely different approach than what I was assuming. Rust doesn't currently support that atm, AFAIK, but I agree it should.

Do you think instruction pointers are actually that useful compared to random identifiers generated by your build system for the purposes of debugging release binaries?

My experience has been that with any inlining debuggers have a hard time recovering much of the stack trace, except for explicit self/mutual recursion and very large functions.

Yes, try! is contained within your crate. If you pass a function pointer or similar to library code, and there is an error in your function, the try approach will still work. If a crate you use has an internal error or bug, you might need its source code in order to debug already, if the returned Err's information does not help. Stack traces are only helpful if you have access to the code, so closed source libraries (whose code you can't modify) will go have to go over support one way or another.

What about simply enabling both approaches, and let the developer decide what's best for them? I do not deny that the TLS based approach doesn't have any advantages.

The try model is implementable very easily, just desugar ? to try, only extra language extensions are needed if catch comes in (you would have added that behaviour inside the hardcoded behaviour of ? anyway)

@eddyb "Do you think instruction pointers are actually that useful compared to random identifiers generated by your build system for the purposes of debugging release binaries?"

That's how debugging native binaries in general works. We (Sentry) are using that almost entirely for the iOS Support at this point. We get a crash dump and resolve the addresses with llvm-symbolizer to the actual symbols.

@mitsuhiko Since we already embed libbacktrace, we could use that to resolve code pointers to source locations, so I'm not entirely opposed to it.

@eddyb yeah. Just looked at the panic code. Would be nice if that was actually an API that Rust code could use. I can see this being useful in a few more places.

About that - I had some thoughts about a typesystem-level trick where {Option,Result}::unwrap would have an extra type argument, defaulting to a type dependent on the location the function was called from, such that the panics from those would have way more useful location information.

Speaking of which...

@glaebhoerl Hah, maybe that's actually worth pursuing then? At least as some unstable experiment.

@eddyb No idea. Probably worth discussing it with some GHCers involved with it first, I only heard about this in passing and googled a link just now. And Rust doesn't have actual implicit parameters in the way that GHC does; whether default type parameters could work instead is an interesting question (probably one you've given more thought to than I).

Just a thought: it'd be useful to have a switch that causes rustc to special-case constructing an Err such that it calls a fn(TypeId, *mut ()) function with the payload before returning. That should be enough to start with basic stuff like filtering errors based on the payload, trapping into a debugger if it sees an error of interest, or capturing backtraces for certain kinds of errors.

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). See this discuss thread for more details.

I'm opposed to extending ? to Options.

The wording of the RFC is pretty clear about the fact that the ? operator is about propagating errors/"exceptions".
Using Option to report an error is the wrong tool. Returning None is part of the normal workflow of a successful program, while returning Err always indicates an error.

If we ever want to improve some areas of errors handling (for example by adding stack traces), implementing ? on Option means that we will have to exclude ? from the changes.

@tomaka could we keep the discussion on the discuss thread? (I've summarized your objection already) I personally find that long discussions on GH get pretty unwieldy, and it'd also be nice to be able to separate discussion this particular point from other future points or questions that may arise.

@eddyb Here's the released-version docs of the implicit callstacks GHC feature I mentioned earlier.

No updates here in a while. Is anybody working to move this forward? Are the remaining tasks in the OP still accurate? Can anything here be E-help-wanted?

I played around last weekend if it would be possible to write a Sentry client for Rust that makes any sense. Since most error handling is actually based on results now instead of panics I noticed that the usefulness of this is severely limited to the point where I just decided to abandon this entirely.

I went to the crates.io codebase as an example to try to integrate an error reporting system into it. This brought me back to this RFC because I really think that unless we can record the instruction pointer somehow as results are passed down and converted in the different stacktraces it will be impossible to get proper error reporting. I already see this being a massive pain just debugging local complex logic failures where I nowadays resort to adding panics where I _think_ the error is coming from.

Unfortunately I currently do not see how the IP could be recorded without massive changes to how results work. Has anyone else played around with this idea before?

We were discussing this in the @rust-lang/lang meeting. Some things that came up:

First, there is definite interest in seeing ? stabilizing as quickly as possible. However, I think most of us would also like to see ? operating on Option and not just Result (but not, I think, bool, as has also been proposed). One concern about stabilizing is that if we stabilize without offering any kind of trait that would permit types other than Result to be used, then it is not backwards compatible to add it later.

For example, I myself write code like this with some regularity:

let v: Vec<_> = try!(foo.iter().map(|x| x.to_result()).collect());

where I am relying on try! to inform type inference that I expect collect to return a Result<Vec<_>, _>. If were to use ?, this inference _might_ fail in the future.

However, in previous discussions we also decided that an amendment RFC was needed to discuss the finer points of any sort of "carrier" trait. Clearly this RFC should be written ASAP, but we'd prefer not to block progress on ? on that discussion.

One thought we had was that if we took @nrc's implementation and we made the trait unstable and implemented it only for Result and some private dummy type, that ought to suppress inference while still only making ? usable with Result.

One final point: I think most of us would prefer that if you use ? on an Option value, it requires that your function's return type must also be Option (not e.g. Result<T,()>). It's interesting to note that this will help with the inference limitations, since we can ultimately infer from the declared return type in many cases what type is required.

The reason for not wanting inter-conversion is that it seems likely to lead to loose logic, sort of analogous to how C permits if x even when x has integral type. That is, if Option<T> denotes a value where None is part of the domain of that value (as it should), and Result<> represents (typically) the failure of a function to succeed, then assuming that None means the function should error out seems suspect (and like a kind of arbitrary convention). But that discussion can wait for the RFC, I suppose.

The reason for not wanting inter-conversion is that it seems likely to lead to loose logic, sort of analogous to how C permits if x even when x has integral type. That is, if Option<T> denotes a value where None is part of the domain of that value (as it should), and Result<> represents (typically) the failure of a function to succeed, then assuming that None means the function should error out seems suspect (and like a kind of arbitrary convention). But that discussion can wait for the RFC, I suppose.

I heartily agree with this.

Another question we had agreed to gate stabilization on was nailing down the contracts that impls of the From trait should obey (or whatever trait we wind up using for Err-upcasting).

@glaebhoerl

Another question we had agreed to gate stabilization on was nailing down the contracts that impls of the From trait should obey (or whatever trait we wind up using for Err-upcasting).

Indeed. Can you refresh my memory and start out with some examples of things you would think should be forbidden? Or perhaps just the laws you have in mind? I have to admit that I am wary of "laws" like this. For one thing, they have a tendency to get ignored in practice -- people take advantage of the actual behavior when it suits their purposes, even if it goes beyond the intended limitations. So that leads to one other question: if we had laws, would we use them for anything? Optimizations? (Seems unlikely to me, though.)

By the way, what is the state of catch expressions? Are they implemeted?

Sadly no :(

On Tue, Jul 26, 2016 at 06:41:44AM -0700, Alexander Bulaev wrote:

By the way, what is the state of catch expressions? Are they implemeted?


You are receiving this because you authored the thread.
Reply to this email directly or view it on GitHub:
https://github.com/rust-lang/rust/issues/31436#issuecomment-235270663

Perhaps you should plan implementing it? There is nothing more depressing than accepted but not implemented RFCs...

cc #35056

FYI, https://github.com/rust-lang/rfcs/pull/1450 (types for enum variants) would open up some interesting ways implement Carrier. For example, something like:

trait Carrier {
    type Success: CarrierSuccess;
    type Error: CarrierError;
}

trait CarrierSuccess {
    type Value;
    fn into_value(self) -> Self::Value;
}

// (could really use some HKT...)
trait CarrierError<Equivalent: CarrierError> {
    fn convert_error(self) -> Equivalent;
}

impl<T, E> Carrier for Result<T, E>
{
    type Success = Result::Ok<T, E>;
    type Error = Result::Err<T, E>;
}

impl<T, E> CarrierSuccess for Result::Ok<T, E> {
    type Value = T;
    fn into_value(self) -> Self::Value {
        self.0
    }
}

impl<T, E1, E2> CarrierError<Result::Err<T, E2>> for Result::Err<T, E1>
    where E2: From<E1>,
{
    fn convert_error(self) -> Result::Err<T, E2> {
        Err(self.into())
    }
}

impl<T> Carrier for Option<T>
{
    type Success = Option::Some<T>;
    type Error = None;
}

impl<T> CarrierSuccess for Option::Some<T> {
    type Value = T;
    fn into_value(self) -> Self::Value {
        self.0
    }
}

impl<T> CarrierError<Option::None> for Option::None {
    fn convert_error(self) -> Option::None {
        self
    }
}

fn main() {
    let value = match might_be_err() {
        ok @ Carrier::Success => ok.into_value(),
        err @ Carrier::Error => return err.convert_error(),
    }
}

I just wanted to cross-post some thoughts from https://github.com/rust-lang/rust/pull/35056#issuecomment-240129923. That PR introduces a Carrier trait with dummy type. The intention was to safeguard -- in particular, we wanted to stabilize ? without having to stabilize its interaction with type inference. This combination of trait plus dummy type seemed like it would be safely conservative.

The idea was (I think) that we would then write-up an RFC discussing Carrier and try to modify the design to match, stabilizing only when we were happy with the overall shape (or possibly removing Carrier altogether, if we can't reach a design we like).

Now, speaking a bit more speculatively, I _anticipate_ that, if we do adopt a Carrier trait, we would want to disallow interconversion between carriers (whereas this trait is basically a way to convert to/from Result). So intuitively if you apply ? to an Option, that's ok if the fn returns Option; and if you apply ? to a Result<T,E>, that's ok if the fn returns Result<U,F> where E: Into<F>; but if you apply ? to an Option and the fn returns Result<..>, that's not ok.

That said, this sort of rule is hard to express in today's type system. The most obvious starting point would be something like HKT (which of course we don't really have, but let's ignore that for now). However, that's not obviously perfect. If we were to use it, one would assume that the Self parameter for Carrier has kind type -> type -> type, since Result can implement Carrier. That would allow us to express things like Self<T,E> -> Self<U,F>. However, it would _not_ necessarily apply to Option, which has kind type -> type (all of this would of course depend on precisely what sort of HKT system we adopted, but I don't think we'll go all the way to "general type lambdas"). Even more extreme might be a type like bool (although I don't want to implement Carrier for bool, I would expect some people might want to implement Carrier for a newtype'd bool).

What I had considered is that the typing rules for ? could themselves be special: for example, we might say that ? can only be applied to a nominal type Foo<..> of _some_ kind, and that it will match the Carrier trait against this type, but it will require that the return type of the enclosing fn is also Foo<..>. So we would basically instantiate Foo with fresh type parameters. The downside of this idea is that if neither the type that ? is being applied to nor the type of the enclosing fn is known, we can't enforce this constraint without adding some new kind of trait obligation. It's also rather ad-hoc. :) But it would work.

Another thought I had is that we might reconsider the Carrier trait. The idea would be to have Expr: Carrier<Return> where Expr is the type ? is applied to and Return is the type of the environment. For example, perhaps it might look like this:

trait Carrier<Target> {
    type Ok;
    fn is_ok(&self) -> bool; // if true, represents the "ok-like" variant
    fn unwrap_into_ok(self) -> Self::Ok; // may panic if not ok
    fn unwrap_into_error(self) -> Target; // may panic if not error
}

Then expr? desugars to:

let val = expr;
if Carrier::is_ok(&val) {
    val.unwrap_into_ok()
} else {
    return val.unwrap_into_error();
}

The key difference here is that Target would not be the _error_ type, but a new Result type. So for example we might add the following impl:

impl<T,U,E,F> Carrier<Result<U,F>> for Result<T,E>
    where E: Into<F>
{
    type Ok = T;
    fn is_ok(&self) -> bool { self.is_ok() }
    fn unwrap_into_ok(self) -> Self::Ok { self.unwrap() }
    fn unwrap_into_error(self) -> { Err(F::from(self.unwrap_err())) }
}

And then we might add:

impl<T> Carrier<Option<T>> for Option<T> {
    type Ok = T;
    fn is_ok(&self) -> bool { self.is_some() }
    fn unwrap_into_ok(self) -> Self::Ok { self.unwrap() }
    fn unwrap_into_error(self) -> { debug_assert!(self.is_none()); None }
}

And finally we could implement for bool like so:

struct MyBool(bool);
impl<T> Carrier<MyBool> for MyBool {
    type Ok = ();
    fn is_ok(&self) -> bool { self.0 }
    fn unwrap_into_ok(self) -> Self::Ok { debug_assert!(self.0); () }
    fn unwrap_into_error(self) -> { debug_assert!(!self.0); self }
}

Now this version is more flexible. For example, we _could_ allow interconversion between Option values to be converted to Result by adding an impl like:

impl<T> Carrier<Result<T,()>> for Option<T> { ... }

But of course we don't have to (and we wouldn't).

@Stebalien

FYI, rust-lang/rfcs#1450 (types for enum variants) would open up some interesting ways implement Carrier

As I was writing up that idea I just wrote, I was thinking about having types for enum variants, and how that might affect things.

One thing I noticed writing some code that uses ? is that it is mildly annoying not to have any kind of "throw" keyword -- in particular, if you write Err(foo)?, the compiler doesn't _know_ that this will return, so you have to write return Err(foo). That's ok, but then you don't get the into() conversions without writing them yourself.

This comes up in cases like:

let value = if something_or_other() { foo } else { return Err(bar) };

Oh, I should add one other thing. The fact that we allow impls to influence type inference _should_ mean that foo.iter().map().collect()?, in a context where the fn returns a Result<..>, I suspect no type annotations would be required, since if we know that the fn return type is Result, only one impl would potentially apply (locally, at least).

Oh, and, a slightly better version of my Carrier trait would probably be:

trait Carrier<Target> {
    type Ok;
    fn into_carrier(self) -> Result<Self::Ok, Target>;
}

where you would implement it like:

impl<T,U,E,F> Carrier<Result<U,F>> for Result<T,E>
    where E: Into<F>
{
    type Ok = T;
    fn into_carrier(self) -> Result<T, Result<U,F>> {
        match self { Ok(v) => Ok(v), Err(e) => Err(e.into()) }
    }
}

And expr? would generate code like:

match Carrier::into_carrier(expr) {
    Ok(v) => v,
    Err(e) => return e,
}

A downside (or upside...) of this of course is that the Into conversions are pushed into the impls, which means that people might not use them when it makes sense. But also means you can disable them if (for your particular type) that are not desired.

@nikomatsakis IMO, the trait should be IntoCarrier and IntoCarrier::into_carrier should return a Carrier (a new enum) instead of re-using result like this. That is:

enum Carrier<C, R> {
    Continue(C),
    Return(R),
}
trait IntoCarrier<Return> {
    type Continue;
    fn into_carrier(self) -> Carrier<Self::Continue, Return>;
}

@Stebalien sure, seems fine.

Nominating for discussion (and possible FCP of the ? operator alone) at the lang team meeting. I assume we need to land some kind of temporary Carrier trait in the next few days to FCP.

I opened rust-lang/rfcs#1718 to discuss the Carrier trait.

Hear ye, hear ye! The ? operator specifically is now entering final comment period. This discussion lasts for roughly this release cycle which began on August 18th. The inclination is to stabilize the ? operator.

With respect to the carrier trait, a temporary version landed in #35777 which should ensure that we have the freedom to decide either way by preventing undesired interaction with type inference.

@rust-lang/lang members, please check off your name to signal agreement. Leave a comment with concerns or objections. Others, please leave comments. Thanks!

  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @aturon
  • [x] @eddyb
  • [ ] @pnkfelix (on vacation)

I wonder if tokio-based libraries will end up using and_then a lot. That would be one argument for letting foo().?bar() be shorthand for foo().and_then(move |v| v.bar()), so that Results and Futures can use the same notation.

Just to be clear, this FCP is about the question_mark feature, not catch, correct? The title of this issue implies the tracking of both of those features in this one issue.

@seanmonstar the latter hasn't even been implemented, so, yeah. Presumably if the FCP results in acceptance this will be changed to track catch.

Just to be clear, this FCP is about the question_mark feature, not catch, correct? The title of this issue implies the tracking of both of those features in this one issue.

Yes, just the question_mark feature.

Following up on https://github.com/rust-lang/rfcs/issues/1718#issuecomment-241764523 . I had thought the current behavior ? could be generalized into "bind current continuation", but the map_err(From::from) part of ? makes it slightly more than just bind :/. If we add a From instance for result altogether, then I suppose the semantics could be v? => from(v.bind(current-continuation)).

There has been a lot of discussion about the relative merits of ? in this internals thread:

https://internals.rust-lang.org/t/the-operator-will-be-harmful-to-rust/3882/

I don't have time to do a deep summary just now. My recollection is that the comments are focused on the question of whether ? is sufficiently visible to draw attention to errors, but I'm probably overlooking other facets of the discussion. If someone else has time to summarize, that would be great!

I haven't commented before and this maybe way too late, but I find the ? operator confusing as well, if it is used as a hidden return statement, as @hauleth pointed out in the discussion you have linked @nikomatsakis.

With try!, it was obvious that there might be a return somewhere, because a macro can do that. With the ? as a hidden return, we would have 3 ways to return values from a function:

  • implicit return
  • explicit return
  • ?

I do like this though, as @CryZe said:

This way it's familiar to everyone, it pipes down the error to the end, where you can handle it, and there's no implicit returns. So it could roughly look like this:

let a = try!(x?.y?.z);

That helps both make the code more concise and doesn't hide a return. And it's familiar from other languages such as coffeescript.

How would making ? resolve on the expression level instead of the function level affect futures? For all other use cases it seems okay for me.

I do like this though, as @CryZe said:

This way it's familiar to everyone, it pipes down the error to the end, where you can handle it, and there's no implicit returns. So it could roughly look like this:

let a = try!(x?.y?.z);

I have postulated this. I think it would be perfect solution.

With try!, it was obvious that there might be a return somewhere, because a macro can do that.

It is only obvious because you're familiar with how macros work in Rust. Which is going to be the exact same once ? is stable, widespread and explained in every intro to Rust.

@conradkleinespel

we would have 3 ways to return values from a function:

  • implicit return

Rust doesn't have "implicit returns", it has expressions that evaluate to a value. This is an important difference.

if foo {
    5
}

7

If Rust had "implicit return", this code would compile. But it doesn't, you need return 5.

Which is going to be the exact same once ? is stable, widespread and explained in every intro to Rust.

For an example of what that might look like, https://github.com/rust-lang/book/pull/134

With try!, it was obvious that there might be a return somewhere, because a macro can do that.
It is only obvious because you're familiar with how macros work in Rust. Which is going to be the exact same once ? is stable, widespread and explained in every intro to Rust.

In any language that I am aware of "macros" mean "here be dragons" and that there anything can happen. So I would rephrase that to "because you're familiar with how macros work", without "in Rust" part.

@hauleth

let a = try!(x?.y?.z);

I have postulated this. I think it would be perfect solution.

I strongly disagree. As you will get a magic symbol that only works in try! and not outside.

@hauleth

let a = try!(x?.y?.z);
I have postulated this. I think it would be perfect solution.
I strongly disagree. As you will get a magic symbol that only works in try! and not outside.

I haven't said that ? should work only in try!. What I was saying is that ? should work like pipe operator that would push data down the stream and return error as soon as it occurred. try! would not be needed in that case, but could be used in the same context as it is used now.

@steveklabnik I think of Rust as a language having implicit return, In your example, 5 was not returned implicitly, but lets take this:

fn test() -> i32 {
    5
}

Here, 5 is implicitly returned, isn't it ? As opposed to return 5; you would need in your example. This makes for 2 different ways to return a value. Which I find somewhat confusing about Rust. Adding a third would not help IMO.

It is not. It's the result of an expression, specifically, the function body. "implicit return" implies that you can somehow implicitly return from anywhere, but that's not true. No other expression-based language calls this an "implicit return", as that would be my code sample above.

@steveklabnik Alright, thanks for taking the time to explain this :+1:

It's all good! I can totally see where you're coming from, it's just two different things that people use in a wrong way often. I have seen people assume "implicit return" means that you can just leave the ; off anywhere in source to return.... that _would_ be very bad :smile:

@hauleth The ? operator would just be syntactic sugar for and_then in that case. That way you could use it in a lot more cases and it wouldn't have to be a hard to miss return. This is also what all other languages do that have a ? operator. Rust's ? operator in the current implementation would be the exact OPPOSITE of what all other languages do. Also and_then is the functional approach and is encouraged anyway, as it has a clear control flow. So just making ? syntactic sugar for and_then and then keeping the current try! for explicitly "unwrapping and returning", seems to be the much cleaner situation, by making the returns more visible and the ? operator more flexible (by being able to use it in non-return cases like pattern matching).

Exactly.

Łukasz Niemier
[email protected]

Wiadomość napisana przez Christopher Serr [email protected] w dniu 02.09.2016, o godz. 21:05:

@hauleth https://github.com/hauleth The ? operator would just be syntactic sugar for and_then in that case. That way you could use it in a lot more cases and it wouldn't have to be a hard to miss return. This is also what all other languages do that have a ? operator. Rust's ? operator in the current implementation would be the exact OPPOSITE of what all other languages do. Also and_then is the functional approach and is encouraged anyway, as it has a clear control flow. So just making ? syntactic sugar for and_then and then keeping the current try! for explicitly "unwrapping and returning", seems to be the much cleaner situation, by making the returns more visible and the ? operator more flexible (by being able to use it in non-return cases like pattern matching).


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub https://github.com/rust-lang/rust/issues/31436#issuecomment-244461722, or mute the thread https://github.com/notifications/unsubscribe-auth/AARzN5-w4EO9_FwNMDpvtYkGUuQKGt-Kks5qmHOHgaJpZM4HUm_-.

And when working on a Pull Request for the Rust repo I actually had to work with code that used the ? operator and in fact it really hurt readability for me, as it just like ; was super hidden (mentally, cause it's just noise that gets filtered out in the brain) and I overlooked it a lot. And I find that quite scary.

@steveklabnik we call it "implicit return", because we aren't the only ones.

@hauleth huh, in all of my years of Ruby, I've never heard of anyone calling it implicit return. I still maintain that it's the wrong way to think about it.

I've used ? in a few projects and have preferred it to try!, mostly because it is in postfix position. In general, "real" Rust code pretty much enters the Result 'monad' at main and never leaves it except at occasional leaf nodes; and such code is just expected to propagate errors always. For the most part it does not matter which expressions are generating errors - they are all just being sent back up the stack, and I don't want to see that when I'm reading through the main logic of the code.

My main concern with ? is that I could get the same benefit - postfix position - with method macros, if they existed. I have other concerns that perhaps by stabilizing the current formulation we are limiting future expressivity in error handling - the current Result conversion is not sufficient to make error handling in Rust as ergonomic as I'd like; we've already made several design mistakes with Rust error handling that look difficult to fix, and this may be digging us in deeper; though I have no concrete evidence.

I wrote this many times before but I am absolutely in love with ? and the possibilities of the carrier trait. I converted one project over to using ? entirely and it made many things possible (particularly with regards to chaining) which was too complex with try!. I also for fun went over some other projects to see how they would do with ? and overall I have not encountered any problems with that.

As such I give a huge +1 to stabilizing ? on the basis of a better named Carrier trait which ideally also covers some of the other cases that I brought up in the other discussion about it.

My main concern with ? is that I could get the same benefit - postfix position - with method macros, if they existed.

Maybe we need an RFC for this? Most people seem to like the functionality of ?, but not the ? itself.

Maybe we need an RFC for this? Most people seem to like the functionality of ?, but not the ? itself.

There _is_ an RFC with lots of discussion for this. Also, I don't know where you're getting that "most people" from. If it's from the participants in this issue, of course you'll see more people arguing against because stabilizing is _already_ the default action of the team.
The ? has been discussed hugely before the RFC was merged, and as a supporter it's kind of tiring to have to do the same thing when stabilization is discussed.

Anyway, I'll put in my +1 for @mitsuhiko's sentiments here.

There is an RFC with lots of discussion for this. Also, I don't know where you're getting that "most people" from. If it's from the participants in this issue, of course you'll see more people arguing against because stabilizing is already the default action of the team.

Sorry, my comment was too brief. I was referring to creating an RFC for some kind of "method macros", for example func1().try!().func2().try!() (As far as I know, this isn't currently possible).

Personally I do like the ? operator, but I share the same concerns as @brson, and I think it it would be good to explore alternatives before we stabilise this feature. Including the RFC conversion, this thread, and the internals thread that @nikomatsakis linked, there is definitely still some contention about this feature, even if it is the same arguments over and over again. However if there are no viable alternatives, stabilising does make the most sense.

It seems premature to stabilize a feature without having it fully implemented -- in this case, the catch { .. } expression.

I have expressed my concerns over this feature before, and I still believe it's a bad idea. I think having a postfix conditional return operator is unlike anything in any other programming language, and is pushing Rust past its already stretched complexity budget.

@mcpherrinm Other languages have instead hidden unwinding paths at every call for error handling, would you call operator() an "conditional return operator"?

As for the complexity budget, it's only syntactically different from try!, at least the part that you're complaining about.
Is the argument against try!-heavy code, which ? only makes more readable?
If so, then I'd agree if there a serious alternative other than "don't have any error propagation automation _at all_".

Suggesting a compromise: https://github.com/rust-lang/rfcs/pull/1737

It may have no chances to get accepted, but I'm trying anyway.

I like @keeperofdakeys's idea about "method macros". I don't think the ? syntax should be accepted for the same reason the ternary operator is not in rust -- readability. The ? itself doesn't say anything. Instead, I would much rather see the ability to generalize ?'s behavior with the "method macros".

a.some_macro!(b);
// could be syntax sugar for
some_macro!(a, b); 
a.try!();
// could be syntax sugar for
try!(a); 

This way, it would be clear what the behavior is, and it allows for easy chaining.

Method macro like result.try!() seems to be a more generic improvement to language ergonomics and feels less ad-hoc than a new ? operator.

@brson

I have other concerns that perhaps by stabilizing the current formulation we are limiting future expressivity in error handling - the current Result conversion is not sufficient to make error handling in Rust as ergonomic as I'd like

This is an interesting point. It'd be worth spending some focused time on this (perhaps you and I can chat a bit). I agree we could do better here. The proposed design for a Carrier trait (see https://github.com/rust-lang/rfcs/issues/1718) may help here, particularly if combined with specialization, since it makes things more flexible.

I really doubt that method macros would be a good extension to the language.

macro_rules! macros are currently declared in an analogous way to free functions, and will become even more analogous when the new import system is adopted for them. What I mean is that they are declared like top level items and invoked like top level items, and soon they will also be imported like top level items.

This is not how methods work. Methods have these properties:

  1. Cannot be declared in a module scope, but must be declared within an impl block.
  2. Are imported with the type/trait the impl block is associated with, rather than imported directly.
  3. Are dispatched on the basis of their receiver type, rather than being dispatched based on being an single, unambiguous symbol in this scope.

Because macros are expanded before typechecking, none of these properties could be true of macros using the method syntax as far as I can tell. Of course we could just have macros that use method syntax but are dispatched and imported the same way as 'free' macros, but I think the disparity would make that a very confusing feature.

For these reasons I don't think its a good choice to delay ? on the belief that "method macros" may someday appear.

Moreover, I think there is a line at which some construct is so widely used and important that it should be promoted from macros to sugar. for loops are a good example. The ? behavior is integral to Rust's error handling story, and I think it is appropriate for it to be first class sugar instead of a macro.

@hauleth, @CryZe

To respond to those suggesting that ? should be an and_then operator, this works well in languages like Kotlin (I'm not familiar with coffeescript) due to their extensive use of extension functions but it's not so straightforward in rust. Basically, most uses of and_then are not maybe_i.and_then(|i| i.foo()), they're maybe_i.and_then(|i| Foo::foo(i)) The former could be expressed as maybe_i?.foo() but the latter can't. One could say that Foo::foo(maybe_i?, maybe_j?) turns into maybe_i.and_then(|i| maybe_j.and_then(|j| Foo::foo(i, j))) but this feels even more confusing than just saying that rust early returns on hitting the first ? that evaluates to an error. However, this would arguably be more powerful.

@Stebalien In the accepted RFC, catch { Foo::foo(maybe_i?, maybe_j?) } does what you want.

@eddyb Good point. I guess I can leave off the "However, this would arguably be more powerful". It comes down to implicit catch/explicit try versus explicit catch/implicit try:

let x: i32 = try Foo::foo(a?.b?.c()?));
let y: Result<i32, _> = Foo::foo(a?.b?.c()?);

Versus:

let x: i32 = Foo::foo(a?.b?.c()?);
let y: Result<i32, _> = catch  Foo::foo(a?.b?.c()?);

(modulo syntax)

@Stebalien Another example: if I wanted to pass Foo to a function bar, with your proposal I'd need to:

bar(Foo::foo(a?.b?.c()?)?)

Is this what you have in mind? Note the extra ?, without it bar would get a Result instead of a Foo.

@eddyb Probably. Note: I'm not actually proposing this! I'm arguing that using ? as a pipe-operator isn't particularly useful in rust without some way to handle the Foo::foo(bar?) case.

Just to note that I hate the idea of method macros and I can't think of a language feature I would oppose more strongly. They fuzz the phasing of the compiler, and unless we make really quite fundamental changes to the language there is no way they can exist and have unsurprising behaviour. They are also hard to parse sensibly and almost certainly not backwards compatible.

@Stebalien, with ? as pipe operator Foo::foo(bar?) would look like this: Foo::foo(try!(bar)) and bar(Foo::foo(a?.b?.c()?)?) (assuming that Foo::foo : fn(Result<_, _>) -> Result<_, _>): bar(try!(Foo::foo(a?.b?.c()?))).

@hauleth my point was that Foo::foo(bar?)? is _much_ more common than bar?.foo()? in rust. Therefore, to be useful, ? would have to support this case (or some other feature would have to be introduced). I was postulating a way to do so and showing that that way at least would be messy. The entire point of ? is to be able to avoid writing try!(foo(try!(bar(try!(baz()))))) (2x the parentheses!); it usually isn't possible to re-write this as try!(baz()?.bar()?.foo()).

But you can always do:

try!(baz().and_then(bar).and_then(foo))

Łukasz Niemier
[email protected]

Wiadomość napisana przez Steven Allen [email protected] w dniu 05.09.2016, o godz. 15:39:

@hauleth https://github.com/hauleth my point was that Foo::foo(bar?)? is much more common than bar?.foo()? in rust. Therefore, to be useful, ? would have to support this case (or some other feature would have to be introduced). I was postulating a way to do so and showing that that way at least would be messy. The entire point of ? is to be able to avoid writing try!(foo(try!(bar(try!(baz()))))) (2x the parentheses!); it usually isn't possible to re-write this as try!(baz()?.bar()?.foo()).


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub https://github.com/rust-lang/rust/issues/31436#issuecomment-244749275, or mute the thread https://github.com/notifications/unsubscribe-auth/AARzN1Hdk6uk5-SoYawtgAbJUDf_8MsMks5qnBumgaJpZM4HUm_-.

On a slightly related note, it seems that the ?-feature is mainly used by builders, so we could possibly avoid the need for the ?-feature by providing an easy way to construct builders that wrap a Result. I proposed something here, but it may need a little more work.

https://github.com/colin-kiegel/rust-derive-builder/issues/25

Thanks for your thoughts on the method macro idea @nrc and @withoutboats, it's good to hear some concrete reasons why they wouldn't work.

@nielsle I don't think its accurate to say that ? is "mainly" being used by builders. While builders are an example where I think the advantage of a lightweight, postfix operator really shines through, I prefer ? to try! in every context.

@nielsle regarding futures, I was originally concerned for similar reasons. But, after thinking about it, I think that async / await would supersede any need for ? in that context. They're actually fairly orthogonal: you could do things like (await future)?.bar().

(Maybe it'd be nice to have a suffix operator instead of the keyword await so parens aren't necessary. Or maybe a carefully-tuned precedence would be enough.)

I would definitely like to see some documentation written before we stabilize. I looked in the reference and couldn't find any mention. Where should we document this feature?

@cbreeden I know @steveklabnik generally avoids documenting unstable features, since there's a chance it will be a waste of time if they are never stabilized. I don't know if we've ever blocked stabilization on documentation writing before.

@solson You're right, this was probably not the place to bring it up -- or at least shouldn't be related to stabilization questions. I guess I was just imagining a situation where we could decide on stabilizing a feature, but then also require documentation before being released to stable rustc. There is an RFC related to integrating documentation with feature stabilization and release, so I'll just wait for that process to stabilize (but not without proper documentation first, of course)

I think the important part of this RFC is to have something of the right side of an expression which acts like try!, because that makes reading sequential/chained usages of "try" _much_ more readable and to have "catch". Originally I was a 100% supporter to use ? as syntax but recently I stumbled about some (clean!) code already using ? which made me aware that outside from simple examples ? is extremely easy overlooked. Which now makes me believe that using ? as syntax for "the new try" might be a big mistake.

Therefore I propose that it might be a good idea to _put up some poll_ before finalizing it (with notification about it on the Forums) to get a feed-back about using ? or some other symbol(s) as syntax. Optimally with example of usage in a longer function. Note that I am only considering that it might be a good idea to rename ? not to change anything else. The poll could list possible other names which did pop up in the past like ?! (or ??) or just something like "use ? vs. use more than on character".

Btw. not using ? might also satisfy the people which don't like it because it is the same syntax as other languages optional type. And those which want to make it a optional syntax for rust Option types. (Through this are not concerns I share).

Additionally I think with such a poll people normally not taking part in the RFC process could be reached. Normally this might not be needed but try! => ? is a very big change for anyone writing and/or reading rust code.

PS:
I put up a gist with the function liked above in different variations ("?","?!","??") through I don't know if there had been more. Also there was a RFC for renaming ? to ?! which was redirected to this discussion.

Sorry, about possible restarting a already long ongoing discussion :smiley_cat: .

(Note that ?? is bad if you still want to introduce ? for Option because expr??? would be ambiguous)

? is extremely easy overlooked

What are we discussing here? Completely unhighlighted code? Regular highlighted code browsing?
Or actively looking for ? in a function?

If I select one ? in my editor, _all_ the other ? in the file are highlighted with a bright yellow background, and the search function also works, so I don't see that last one as posing _any_ difficulty for me.

As for other cases, I'd rather solve this by better highlighting than end up with ?? or ?!.

@dathinab I think .try!() or something would be even better, but that would require UFCS for macros.

let namespace = namespace_opt.ok_or(Error::NoEntry).try!();

That way it's hard to miss, but just as easy, if not even easier to type than .unwrap().

@eddyb: it's about normal highlighted code, e.g. on github. E.g. when reading through a codebase. From my point of view it feels kind of wrong if I need strong highlighting to not easily overlook a ? which would both introduce another return path (/path to catch) and possible changes the type of an variable from Result<T> to T

@CryZe: I agree with you but I don't think we can get this in the near future. And having something which is a little bit shorter than .try!() isn't so bad either.

@CryZe I like that syntax too, but @withoutboats mentioned some solid reasons why method macros might hurt the language.
On the other hand, I fear that if method macros do appear, I don't think they would play well with ?.

I'm not inherently against using two characters for this sigil, but I looked at the examples & I didn't find the change made ? seem any more visible to me.

Yeah same. I think it needs to be some kind of keyword, as symbols in general are used more for structuring, both in normal language and programming languages.

@CryZe: Maybe something like ?try would be kind of a keyword. But to be honest I don't like it (?try).

@eddyb

and the search function also works

Searching for ? will turn up false positives, while longer versions most likely won't.

Searching for ? will turn up false positives, while longer versions most likely won't.

I expect that syntax highlighters will take care of this internally (avoiding false positives). As a matter of fact, they can even insert some form of "might return" marker in the fringe (next to the line numbers).

For example,
screen-2016-09-15-175131

Wow that definitely helps a lot. But then you really need to make sure you
have your editor configured properly, which you can't do all the time (like
on GitHub for example).

2016-09-15 23:52 GMT+02:00 Steven Allen [email protected]:

For example,
[image: screen-2016-09-15-175131]
https://cloud.githubusercontent.com/assets/310393/18568833/1deed796-7b6d-11e6-99af-75f0d7ddd778.png


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rust/issues/31436#issuecomment-247465972,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABYmbjyrt07NXKMUdmlBfaciRZq7uBVEks5qqb4sgaJpZM4HUm_-
.

@CryZe I'm pretty sure GitHub is just using some open source syntax highlighter; we can patch it :-).

I think in general it is a good idea for the Rust project to make a recommendation that syntax highlighters for Rust should use a highly visible style on ?, to balance the concerns about visibility.

I think ? is the best choice we can make here, and we've been using it pretty heavily in Miri.

Hearkening back to the original motivation, try!(...) is obtrusive and makes it difficult to ignore the flow of errors and just read the happy-path. This is a downside compared to the invisible exceptions of traditional languages. Expanding ? to a more involved keyword would have the same downside.

On the other hand, with ?, when I don't care about the flow of errors I can ignore it and let it fade into the background. And when I really do care about the flow of errors, I can still see ? just fine. Highlighting ? brightly is not even necessary for me, but if it helps other people, that's great. This is an improvement over both invisible exceptions and try!.

Switching to a trivially larger sigil like ?! would not aid me in any way, but make reading and writing error handling code marginally worse.

Thanks everyone for a hearty final comment period (as well as a prior internals thread). Given that we're talking about the most commented RFC to date, I'm not surprised to see that the discussion around stabilization has also been quite active.

Let me cut to the chase first: The @rust-lang/lang team has decided to stabilize the ? operator when applied to values of Result type. Note that the catch feature is not being stabilized (and, indeed, has not yet been implemented); similarly, the so-called "carrier trait", which is a means to extend ? to types like Option, is still in the pre-RFC discussion phase. We have however taken steps in the current implementation to ensure that we can add the Carrier trait later (which address some of my earlier concerns about potential interaction with inference).

I'd like to take a bit of time to summarize the discussion that has taken place since the FCP began on Aug 22. Many of these themes also occurred in the original RFC thread. If you're interested in reading the thread, the FCP comment and recap comment in that thread attempt to cover the conversation in depth. In some cases I will link to comments in that original thread if they are more in-depth than the corresponding ones from this thread.

The scope of the ? operator ought to be the current expression, not the current function.

When an error occurs, today's try! macro propagates that error unconditionally to the calling function (in other words, it executes a return with the error). As currently designed, the ? operator follows this precedent, but with the intention of supporting a catch keyword that allows the user to specify a more limited scope. This means, for example, that x.and_then(|b| foo(b)) can be written as catch { foo(x?) }.In contrast, several recent languages use the ? operator to mean something more analogous to and_then, and there has been concern that this may prove confusing to new users.

Ultimately, once catch is implemented, this is a question of defaults. And there are several reasons that we believe that the default of "break out of function" (with the option to customize) is more appropriate for ? in Rust:

  • the try! macro has proven itself many times over as a very useful default. ? is intended as a replacement for try!, so it's natural for it to behave the same.
  • ? in Rust is primarily in use for propagating results, which is more analogous to exceptions. All exception systems default to propagating errors out of the current function and into the caller (even those which, like Swift, also require a keyword to do so).

    • in contrast, ? in Swift, Groovy, and so forth has to do with null values or option types.

    • if we adopt a carrier trait, the ? operator in Rust can still be used in said scenarios (and in particular in conjunction with catch), but it's not the _main_ use case.

  • many uses of and_then in Rust today would not work using method notation; some foreseen future uses (such as in futures) may be subsumed by an async-await transformation in any case

The ? obscures control flow because it is hard to spot.

A common concern is that the ? operator is too easy to overlook. This is clearly a balancing act. Having a lightweight operator makes it easy to focus on the "happy path" when you want to -- but it's important to have some indication of where errors can occur (in contrast to exceptions, which introduce implicit control-flow). Moreover, it is easy to make ? easier to spot through syntax highlighting (e.g., 1, 2).

Why not method macros?

One of the big benefits of ? is that it can be used in post-fix position, but
we could obtain similar benefits from "method macros" like foo.try!. While true, method macros open up a lot of complexity themselves, particularly if you want them to behave like methods (e.g., dispatched based on the type of the receiver, and not using lexical scope). Moreover, using a method macro like foo.try! has a significantly heavier weight feel than foo? (see the previous point).

What contracts should From offer?

In the original RFC discussion, we decided to postpone the question of [whether there should be "contracts" for From]((https://github.com/rust-lang/rust/issues/31436#issuecomment-180558025). The general consensus of the lang team is that one should view the ? operator as invoking the From trait, and that the From trait impls can naturally do whatever is permitted by their type signatures. Note that the role of the From trait is quite limited here anyway: it is simply used to convert from one sort of error to another (but always in the context of a Result).

However, I would like to note that the discussion regarding a "carrier" trait is ongoing, and adopting strong conventions are more important in that scenario. In particular, the carrier trait gets to define what constitutes "success" and "failure" for a type, as well as whether one kind of value (e.g., an Option) can be converted into another (e.g., a Result). Many have argued that we do not want to support arbitrary interconversions between "error-like" types (e.g., ? should not be able to convert Option to Result). Obviously, since end-users can implement the Carrier trait for their own types as they choose, this is ultimately a guideline, but I think it's an important one.

port try! to use ?

I don't think we can ever do this backwards compatibly and we should leave the implementation of try! alone. Should we deprecate try!?

@nrc Why isn't it backward compatible?

@withoutboats try!(x) is (x : Result<_, _>)? and we could probably implement it that way if we _wanted_ to, but in general x? could infer to anything that supports the Carrier trait (in the future), one example is iter.collect()? which with try! would only be Result but can realistically be Option.

That makes sense. I thought we accepted that adding impls to std could cause inference ambiguities; why not also accept that here?

Either way, I do think try! should be deprecated.

? is more useful in a builder pattern like context, while try is more useful in a nested method like context. I think it should not be deprecated, whatever that means.

@est31 They do exactly the same thing right now except for inference. Not sure what you mean exactly, but ? is usually strictly cleaner (again modulo inference). Could you give examples?

@eddyb foo()?.bar()?.baz() is nicer than try!(try!(foo()).bar()).baz(), and try!(bar(try!(foo()))) is nicer than bar(foo()?)?

I find ? more readable in both cases. It reduces the unnecessary parentheses clutter.

When will this land on stable?

@ofek this is for the whole thing, which is not yet complete, so it's hard to say. https://github.com/rust-lang/rust/pull/36995 stabilized the basic ? syntax, which should be stable in 1.14.

Don't forget to add the ? operator to the reference now that its stable: https://doc.rust-lang.org/nightly/reference.html#unary-operator-expressions

And @bluss pointed out that the book is outdated as well: https://doc.rust-lang.org/nightly/book/syntax-index.html

@tomaka

I'm opposed to extending ? to Options.

It doesn't have to be used for error purposes. For example, if I hape a wrapper method for taking get and mapping it by some function, then I'd like to be able to propagate the None case up.

? was pitched as being just for errors; the more general do notation for general propagation like this was a non-goal.

On Oct 29, 2016, 11:08 -0400, ticki [email protected], wrote:

@tomaka (https://github.com/tomaka)

I'm opposed to extending ? to Options.

It doesn't have to be used for error purposes. For example, if I hape a wrapper method for taking get and mapping it by some function, then I'd like to be able to propagate the None case up.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub (https://github.com/rust-lang/rust/issues/31436#issuecomment-257096575), or mute the thread (https://github.com/notifications/unsubscribe-auth/AABsipGIpTF1-7enk-z_5JRYYtl46FLPks5q42DCgaJpZM4HUm_-).

This (= ? itself) is now a stable feature! (From Rust 1.13)

Two documentation issues:

  • [x] Update Error Handling chapter in the book #37750
  • [x] Update Carrier trait docs for current situation #37751

Note that neither catch nor the Carrier trait have been properly implemented yet, just the bare ? feature.

Carrier exists, and error messages refer to it when using ?, so it would be better if the Carrier issue had been fixed rather than rejected. Its docs also need updating, since the docs refer to it being implemented for Option. (Which is false).

35946 should remove any mention of Carrier from the error messages. It does sound like we should at least remove the Option mention from the Carrier docs.

I'm adding T-libs to this issue due to the interaction with the Carrier trait

Hi; in #31954 the upcast for the error type is done using From (and similar the same in current head), but RFC 243 clearly states Into should be used for the conversion.

Is there any reason why From was used instead? When trying to upcast to some generic error type (i.e. io::Error, or something else in an external crate) From cannot be implemented for local error types.

(FWIW as the author of RFC 243 I didn't think very hard about whether From or Into is preferable, and may or may not have made the right choice. Which is just to say that the question should be decided based on the merits (which at this point may include backwards compatibility), rather than what's written in the RFC.)

Sadly there would be a regression. If no (non-trivial) From<...> instance for an error type is implemented, the compiler can deduce the type in certain cases, where it cannot deduce it when using Into (the set of From instances is limited by the current crate, the full set of Into instances is not known while compiling a crate).

See https://play.rust-lang.org/?gist=6d3ee9f93c8b40094a80d3481b12dd00 ("simplified" from a real world problem involving fmt::Error in src/librustc/util/ppaux.rs#L81)

There's a new RFC which will supersede the old in terms of describing ?: rust-lang/rfcs/pull/1859

I’m -1 on having catch blocks at all, when they are much more concise already with the closures.

Closures interfere for flow control statements. (break, continue, return)

Now that https://github.com/rust-lang/rfcs/pull/1859 has been merged, and claims that we are re-using this issue as its tracking issue, I would like to propose that we reevaluate the catch portion of RFC 243.

I'm not proposing that we get rid of it outright, but enthusiasm for catch was never as great as it was for ? and I think it's worth making sure that it still makes sense in light of the idioms that have emerged for ?, and that are expected to emerge in light of Try.

I'm still eager to have catch; earlier today I had to contort some code in a way that could have been avoided if catch were stable.

The feature has been implemented on nightly with the syntax do catch, hasn't it?

@withoutboats Yes, it's currently do catch, since catch { ... } conflicts with struct literals (struct catch { }; catch { }).
.
@archshift As @SimonSapin pointed out above, closures interfere with break, continue, and return.

@bstrie I find that I pretty frequently want catch these days; it comes up a lot during refactoring.

I didn't realize that we were planning on requiring do catch as the syntax. Given that the risk of real-world breakage seems exceedingly low (both violates the struct naming guidelines and would have to have the constructor be the first expression in the statement (which is rare outside of return position)), could we perhaps leverage rustfmt to rewrite any offending identifiers to catch_? If it requires Rust 2.0 to do so, then, well, I've always been one to say that Rust 2.0 ought to contain only trivial breaking changes anyway... :P

@bstrie We absolutely don't want to require do catch long term. The discussion that led to using that syntax for now is here: https://github.com/rust-lang/rust/pull/39921

Excellent, thanks for the context.

I just came here because I hoped catch was stable, and had to learn it is not -- so yes, absolutely, now that ? is stable, it'd be great to also have catch.

I wanted to see how far we'd gotten on the rest of this and saw the do catch discussion.

I've been considering a probably silly idea that would help in cases like this: allow optionally prefixing keywords with @ or some other sigil, then make all new keywords use the sigil only. We also had a similar problem with the coroutine discussion. I can't remember if I went into this as a solution there--I might have--but it looks like this may keep coming up.

FWIW, C# actually supports the opposite: @ for using keywords as identifiers. That's commonly seen in Razor, where you pass things like new { @class = "errorbox" } to set properties on HTML nodes.

@scottmcm
That's interesting. I didn't know about it. But for Rust we have to go the other way because compatibility.

Neat idea on their part, though. The .net ecosystem has a lot of languages, all with disparate keywords and all able to call each other.

One other possibility for the future of keywords: put them behind an attribute, like some sort of stable #[feature].

I think this problem will need a more general solution in the long run.

@camlorn This thread may interest you, specifically Aaron's idea about Rust "epochs": https://internals.rust-lang.org/t/pre-rfc-stable-features-for-breaking-changes/5002

I think we should link https://github.com/rust-lang/rust/issues/42327 from the issue description here. (Also maybe the RFC text should be updated to link there instead of here.)

(EDIT: I posted a comment there, not sure who is or isn't already subscribed to it!)

@camlorn It could, however, open up the "rustfmt does a trivial rewrite for a while, then it becomes a keyword" path. AKA take advantage of the "it's not breaking if there's an extra annotation that could have been written that would make it work in both" escape hatch in the stability guarantee. And, similar to UFCS for inference changes, a hypothetical "store in fully-elaborated form" model could keep old crates working.

I wouldn't mind if the syntax for catch block was just do { … } or even ?{ … }

do has the nice property of already being a keyword. It has the dubious (depending on your perspective) property of invoking Haskell-like do-notation, though that never stopped its past uses, and this one is somewhat closer in use case.

it also looks like, but behaves different than javascripts proposed do expressions

That proposal isn't really relevant to Rust, which already uses bare blocks as expressions.

I was just pointing out the possible confusion since both would look the same but do entirely different things.

It has the dubious (depending on your perspective) property of invoking Haskell-like do-notation

@rpjohnst Result and Option are monads, so it's at least conceptually similar. It should also be forward-compatible to extend it in the future to support all the monads.

Right, in the past the objection has been that if we add do for something conceptually similar to monads, but without fully supporting them, it would make people sad.

At the same time, while we could probably make it forward-compatible with full do-notation, we probably shouldn't add full do-notation. It is not composable with control structures like if/while/for/loop or break/continue/return, which we must be able to use inside and across catch (or in this case do) blocks. (This is because full do-notation is defined in terms of higher order functions, and if we stuff the contents of a catch block into a series of nested closures, suddenly control flow all breaks.)

So in the end this downside of do is that it looks like do-notation without actually being do-notation, and without a good path forward to becoming do-notation either. Personally, I'm totally fine with this because Rust isn't gonna get do notation anyway- but that's the confusion.

@nikomatsakis , #42526 is merged, you can mark it as done in tracking list :)

Is it possible to make the catch keyword contextual but disable it if a struct catch is in scope, and issue a deprecation warning?

Not sure how appropriate this is but I ran into an issue which maybe this needs to solve in that sometimes you want to abort an ok rather than error value which is currently possible when you use return insofar that you often want to abort an _inner_ error through an _outer_ okay. Like when a function say returns Option<Result<_,_>> which is quite common from say an iterator.

In particular I have a macro in a project which I use copiously right now:

macro-rules! option_try {
    ( $expr:expr ) => {
        match $expr {
            Ok(x)  => x,
            Err(e) => return Some(Err(e.into())),
        }
    }
}

It's very common that a function called within an Iterator::next implementation needs to immediately abort with Some(Err(e)) on failure. This macro works inside of a normal function body but not inside of a catch block because catch doesn't grab return categorically but just the special ? syntax.

Though in the end labeled-returns would make the entire catch block idea redundant wouldn't they?

It looks like #41414 is done. Could someone update the OP?

Update: RFC rust-lang/rfcs#2388 is now merged and so catch { .. } is to be replaced with try { .. }.
See the tracking issue right above this comment.

What does this mean for Edition 2015, is the do catch { .. } syntax still on a path towards stabilization, or will this be dropped and only supported via try { .. } in Edition 2018+?

@Nemo157 The latter.

Is there a two-statement try ... catch ... in the current proposal? If so, I don’t get the semantics.

Anyway, if this proposal is only about desugaring then I’m cool with it. i.e. Does a catch block just change where the ? operator exits to?

As all check boxes are ticked in the top post, when would us move forward? If there are still unresolved issues we need to have new check box(es) added.

There are probably plenty of remaining unresolved questions not recorded.
For example, the ok-wrapping behavior is not settled within the lang team, the design of the Try is not finalized, and so on. We should probably split this issue up into several more targeted ones as it has likely outlived it's usefulness.

Hmm... it bothers me that these questions haven't been recorded.

@mark-i-m So to clarify, I think they have been somewhere; but not in one location; it's a bit scattered atm in various RFCs and issues and such, so what we need to do is record them in the proper locations.

The design of the backing trait is tracked in https://github.com/rust-lang/rust/issues/42327; there's extensive discussion there about weaknesses in the current one and a possible new direction. (I'm planning of making a pre-RFC for a change there once 2018 settles a bit.)

So I think only try{} is left here, and the only disagreement I know of for that are things that were settled in the RFC and re-confirmed in one of the above-mentioned issues. It could still be good to have a distinct tracking issue, though.

I'll add a checkbox for the one pending implementation task I know still needs to be done...

@scottmcm I know @joshtriplett had concerns about OK-wrapping (noted in the try RFC) and I'd personally like to restrict break in the initial stabilization of try { .. } so that you can't do loop { try { break } } and such.

@Centril

so that you can't do loop { try { break } }

Right now, you cannot use break in a non-loop block, and it is correct: break should only be used in loops. To early leave a try block, the standard way is to write Err(e)?. and it forces that early leaves are always in the "abnormal" control path.

So my proposal is the code you shown should be allowed, and it should break the loop, not just leaving the try.

The immediate benefit, is when you see break you know it is going break from a loop, and you can always replace it with a continue. Also, it removes the need to having to label the break point when using try blocks inside a loop and you want to exit the loop.

@Centril Thank you for raising those.

Regarding break, I personally would be fine with simply saying that try doesn't care about break and it passes through to the containing loop. I just don't want break to interact with try at all.

As for Ok-wrapping, yes, I'd like to address that before stabilizing try.

@centril Yes, I'm aware. But it's important to remember that that's re-re- raising the issue. The RFC decided to have it, it was implemented without it, but then the original intent was taken _again_, and the implementation changed to follow the RFC. So my big question is whether any material facts have changed, especially given that this is one of the noisiest topics I've ever seen discussed on RFCs+IRLO.

@scottmcm Of course, as you know, I agree with retaining Ok-wrapping ;) and I agree that the issue should be considered settled.

I just wanted to comment on this, not sure if this is the right thing:

Essentially, a situation I have is callbacks in a GUI framework - instead of returning an Option or Result, they need to return a UpdateScreen, to tell the framework if the screen needs to be updated or not. Often I don't need logging at all (it's simply not practical to log on every minor error) and simply return a UpdateScreen::DontRedraw when an error has occurred. However, with the current ? operator, I have to write this all the time:

let thing = match fs::read(path) {
    Ok(o) => o,
    Err(_) => return UpdateScreen::DontRedraw,
};

Since I can't convert from a Result::Err into a UpdateScreen::DontRedraw via the Try operator, this gets very tedious - often I have simple lookups in hash maps that can fail (which isn't an error) - so often in one callback I have 5 - 10 usages of the ? operator. Because the above is very verbose to write, my current solution is to impl From<Result<T>> for UpdateScreen like this, and then use an inner function in the callback like this:

fn callback(data: &mut State) -> UpdateScreen {
     fn callback_inner(data: &mut State) -> Option<()> {
         let file_contents = fs::read_to_string(data.path).ok()?;
         data.loaded_file = Some(file_contents);
         Some(())
     }

    callback_inner(data).into()
}

Since the callback is used as a function pointer, I can't use an -> impl Into<UpdateScreen> (for some reason, returning an impl is currently not allowed for function pointers). So the only way for me to use the Try operator at all is to do the inner-function trick. It would be nice if I could simply do something like this:

impl<T> Try<Result<T>> for UpdateScreen {
    fn try(original: Result<T>) -> Try<T, UpdateScreen> {
        match original {
             Ok(o) => Try::DontReturn(o),
             Err(_) => Try::Return(UpdateScreen::DontRedraw),
        }
    }
}

fn callback(data: &mut State) -> UpdateScreen {
     // On any Result::Err, convert to an UpdateScreeen::DontRedraw and return
     let file_contents = fs::read_to_string(data.path)?;
     data.loaded_file = Some(file_contents);
     UpdateScreen::Redraw
}

I am not sure if this would be possible with the current proposal and just wanted to add my use-case for consideration. It would be great if a custom Try operator could support something like this.

EDIT:
I made a mistake.


Ignore this post


Could this play better with type inference, it fails even in simple cases.

fn test_try(a: u32, b: u32) {
    let div = if b != 0 {
        Some(a / b)
    } else {
        None
    };

    let x // : Option<_> // why is this type annotation necessary
    = try { div? + 1 };

    println!("{:?}", x);
}

If this is re-written to use a closure instead of the try block (and in the process loose auto wrapping), then we get

fn test_closure(a: u32, b: u32) {
    let div = if b != 0 {
        Some(a / b)
    } else {
        None
    };

    let x =  (|| (div? + 1).into())();

    println!("{:?}", x);
}

Which doesn't require a type annotation, but it does require that we wrap the result.

playground

@KrishnaSannasi your closure based example has a type inference failure as well (playground) because Into doesn't constrain the output and you don't use it anywhere that does later.

This seems to mostly be an issue with the Try trait rather than try blocks, similar to Into it doesn't propagate any type information from the inputs to the output, so the output type must be determinable by its later usage. There's a _lot_ of discussion in https://github.com/rust-lang/rust/issues/42327 about the trait, I haven't read it so I'm not sure if any of the proposals there could fix this issue.

@Nemo157

Yes, I did a last minute change to my code, to make it use into, and didn't test that. My bad.

How far are we from stabilizing try blocks? It's the only feature i need from nightly :D

@Arignir

I believe once this is done, it can be stabilized.

block try{}catch (or other following idents) to leave design space open for the future, and point people to how to do what they want with match instead

Isn't there any middle ground design to allow the feature now while still let the possibility to leave design space open for the future (and therefore an eventual catch block)?

The PR I made should check off that box anyway, CC @nikomatsakis

I tried using this for the first time yesterday, and I was a little surprised that this:

#![feature(try_blocks)]
fn main() -> Result<(), ()> {
    let x: () = try {
        Err(())?
    }?;
    Ok(x)
}

does not compile due to

error[E0284]: type annotations required: cannot resolve `<_ as std::ops::Try>::Ok == _`

Rather, I had to do

#![feature(try_blocks)]
fn main() -> Result<(), ()> {
    let x: Result<(), ()> = try {
        Err(())?
    };
    let x = x?;
    Ok(x)
}

instead.

This was confusing at first, so maybe it is worth changing the error message or mentioning in --explain?

If you move the question mark in your first example down a bit to

#![feature(try_blocks)]
fn main() -> Result<(), ()> {
    let x: () = try {
        Err(())?
    };
    Ok(x?)
}

You get a better error message. The error comes up because Rust can't decide which type to resolve the try { ... } to, due to how general it is. Because it can't resolve this type, it can't know what the <_ as Try>::Ok type is, which is why you got the error that you did. (because the ? operator unwraps the Try type and gives back the Try::Ok type). Rust can't work with the Try::Ok type on it's own, it must be resolved through the Try trait, and the type that implements that trait. (which is a limitation of the current way type checking works)

Everything for this feature is implemented, correct? If so, how long do we want to look at sitting on this before stabilizing?

I thought it was still an open question of whether we wanted this or not. In particular, there was some discussion about whether we want to use the language of exceptions here (try, catch).

Personally, I’m strongly against trying to create the impression that Rust has something like exceptions. I think the use of the word catch in particular is a bad idea because anyone coming from a language with exceptions will assume this does unwinding, and it doesn’t. I would expect it to be confusing and painful to teach.

In particular, there was some discussion about whether we want to use the language of exceptions here (try, catch).

I think https://github.com/rust-lang/rfcs/pull/2388 definitively settled whether try as a name is acceptable. This is not an open question. But the definition of the Try trait as well as Ok-wrapping seem to be.

Ok-wrapping was already decided in the original RFC, then removed during implementation, and finally re-added later. I don't see how it's an open question.

@rpjohnst Well it is by virtue of Josh disagreeing with the decision with the original RFC... :) It's a settled matter for me. See https://github.com/rust-lang/rust/issues/31436#issuecomment-427096703, https://github.com/rust-lang/rust/issues/31436#issuecomment-427252202, and https://github.com/rust-lang/rust/issues/31436#issuecomment-437129491. Anyways... the point of my comment was that try as "language of exceptions" is a settled matter.

Woah, when did this happen? The last thing I remember was discussions on internals. Im very much against Ok-wrapping too :(

Eww. Can't believe this happened. Ok-wrapping is so horrible (it breaks the very sensible intuition that all return expressions in a function should be of the function's return type). So yeah, definitely with @mark-i-m on this. Is Josh's disagreement enough to keep this an open issue, and get more discussion on it? I'd gladly lend him support in fighting this, not that it means something as a non- team member.

Ok-wrapping as accepted in RFC 243 (literally the one that defined the ? operator, if you were wondering when this happened) does not change anything about function return expressions' types. This is how RFC 243 defined it: https://github.com/rust-lang/rfcs/blob/master/text/0243-trait-based-exception-handling.md#catch-expressions

This RFC also introduces an expression form catch {..}, which serves to "scope" the ? operator. The catch operator executes its associated block. If no exception is thrown, then the result is Ok(v) where v is the value of the block. Otherwise, if an exception is thrown, then the result is Err(e).

Note that catch { foo()? } is essentially equivalent to foo().

That is, it takes a block of type T and unconditionally wraps it to produce a value of type Result<T, _>. Any return statement in the block is completely unaffected; if the block is the tail expression of a function the function must return a Result<T, _>.

It's been implemented this way on nightly for ages: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=88379a1607d952d4eae1d06394b50959. This was done after much discussion by the lang team in, and linked from, this thread: rust-lang/rust#41414 (and this is linked at the top of this issue as well).

On May 28, 2019 5:48:27 PM PDT, Alexander Regueiro notifications@github.com wrote:

Eww. Can't believe this happened. Ok-wrapping is so horrible (it
breaks the very sensible intuition that all return expressions in a
function should be of the function's return type). So yeah, definitely
with @mark-i-m on this. Is Josh's disagreement enough to keep this an
open issue, and get more discussion on it? I'd gladly lend him support
in fighting this, not that it means something as a non- team member.

Thank you. I'm not just disagreeing with this solely for myself; I'm also representing the numerous people I've seen express the same position I do.

@joshtriplett @mark-i-m @alexreg

Can one of you explain why you find Ok wrapping to be so disagreeable or provide a link to somewhere that it has been explained before? I went looking, but in a cursory view I didn't see anything. I have no horse in this (I quite litterally only commented on this because I saw all the boxes check and no discussion for a month), but now that I have kicked this hornet's nest I want to understand the arguments better.

On Tue, May 28, 2019 at 03:40:47PM -0700, Russell Johnston wrote:

Ok-wrapping was already decided in the original RFC, then removed during implementation, and finally re-added later. I don't see how it's an open question.

I think you partly answered your own question. I don't think everyone
involved in the original RFC discussion was on the same page; try was
absolutely something many people wanted, but there was not consensus
for Ok-wrapping.

On Tue, May 28, 2019 at 03:44:46PM -0700, Mazdak Farrokhzad wrote:

Anyways... the point of my comment was that try as "language of exceptions" is a settled matter.

As a clarification, I don't find the metaphor of "exceptions" appealing,
and many of the attempts at things like try-fn and Ok-wrapping seem to
attempt to make the language fake having an exception-like mechanism.
But try itself, as a means of catching ? at something other than the
function boundary, makes sense as a control-flow construct.

On Tue, May 28, 2019 at 11:37:33PM -0700, Gabriel Smith wrote:

Can one of you explain why you find Ok wrapping to be so disagreeable

As one of a few reasons:

On Tue, May 28, 2019 at 05:48:27PM -0700, Alexander Regueiro wrote:

it breaks the very sensible intuition that all return expressions in a function should be of the function's return type

This breaks various approaches people use for type-directed reasoning
about functions and code structure.

I certainly have my own thoughts here, but could we please not re-open this topic right now? We just had a contentious 500+ post conversation about syntax, so I'd like to avoid landmines for a while.

If this is blocked on the lang team discussing that, should the "resolve whether catch blocks should "wrap" result value (#41414)" checkbox be unchecked again (maybe with a comment that it's blocked on lang team) so that people looking at this tracking issue know the status?

Apologies, I'm not trying to reopen anything- just restate what's marked as decided in the tracking issue and when+how that happened.

@rpjohnst Thanks for the info!

@yodaldevoid Josh pretty much summarized my thoughts.

I am slightly less opposed to ok-wrapping confined to a block (as opposed to affecting the type of a function), but I think it still sets a bad precedent: as Josh said “I don’t find the metaphor of exceptions appealing”

@joshtriplett has essentially summarised my views too: the issues are the suitability of the "exception" metaphor (arguably panics + catch_unwind is much more analagous) and type-based reasoning. I am indeed okay with try blocks as a scoping & control-flow mechanism too, but not the more radical points.

Okay, fair enough, let's not have the whole debate here... maybe just uncheck the box as suggested, and put it back to lang-team debate (in their own time), using some of the rationale mentioned in this thread? As long as stabilisation is not rushed, that sounds reasonable I suppose.

Has a syntax for type annotations been agreed? I was hoping for some try { foo()?; bar()?; }.with_context(|_| failure::err_msg("foon' n' barn'")?;, which is not even remotely interested in compiling: error[E0282]: type annotations needed.

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=4e60d44a8f960cf03307a809e1a3b5f2

I read through the comments a little while ago (and loading 300 comments again on github is way too tedious), but I do recall that most (if not all) of the examples regarding the debate around Try::Ok wrapping used Ok in the example. Considering Option implements Try as well, I would like to know how that impacts the team's position on which side of the debate to be on.

Every time I use Rust, I keep thinking "man, I really wish I could use a try block here," but about 30% of the time that's because I really wish I could use try for Options (like I used to in Scala, which used the for syntax to apply to monads in general, but is very similar to try here).

Just today, I was using the json crate and it exposes the as_* methods which return options.

Using the two syntaxes, my example would've been:

match s {
  "^=" => |a, b| try { a.as_str()?.starts_with(b.as_str()?) }.unwrap_or(false),
  "$=" => |a, b| try { Some(a.as_str()?.ends_with(b.as_str()?)) }.unwrap_or(false),
  // original
  "$=" => |a, b| {
    a.as_str()
      .and_then(|a| b.as_str().map(|b| (a, b)))
      .map(|(a, b)| a.starts_with(b))
      .unwrap_or(false)
    },
}

I think that, contextually, whether or not the return type is Option or Result is pretty clear, and moreso, it doesn't really matter (as far as code comprehension goes). Transparently, the meaning is clear: "I need to check if these two things are valid and do an operation on them." If I had to pick one of these, I would go with the first, because I don't believe that there is any loss of understanding when you consider that this function is embedded in a larger context, as try will always be.

When I first started looking at this thread, I was against Ok wrapping because I thought it would be better to be explicit, but since then, I've started paying attention to the times that I said "I wish I could use a try block here" and I've come to the conclusion that Ok-wrapping is good.

I originally thought that not Ok wrapping would be better in the case where your last statement is a function which returns the type which implements Try, but the difference in syntax would be

try {
  fallible_fn()
}

try {
  fallible_fn()?
}

And in this case, I again think Ok-wrapping is better because it makes it clear that fallible_fn is a Try returning function, so it's actually more explicit.

I want to know what the opposition thinks of this, and, since I can't see many others in this thread, @joshtriplett.

EDIT: I should mention I was only looking at this from an ergonomics/reading comprehension perspective. I have no idea if one has more technical merits than the other in terms of implementation, such as easier inference.

I wanted to give try a shot for some nested Option parsing as well:

#![feature(try_blocks)]

struct Config {
    log: Option<LogConfig>,
}

struct LogConfig {
    level: Option<String>,
}

fn example(config: &Config) {
    let x: &str = try { config.log?.level? }.unwrap_or("foo");
}

This fails with

error[E0282]: type annotations needed
  --> src/lib.rs:12:19
   |
12 |     let x: &str = try { config.log?.level? }.unwrap_or("foo");
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type
   |
   = note: type must be known at this point

The closest I got was

fn example(config: &Config) {
    let x: Option<&str> = try { &**config.log.as_ref()?.level.as_ref()? };
    let x = x.unwrap_or("foo");
}

The as_ref is quite unfortunate. I know that Option::deref will help some here, but not enough. This feels like somehow match ergonomics (or related idea) should come into play.

The multiple lines is also unfortunate.

Could try use an inference fallback of Result like integer literals? Would that let @shepmaster's first attempt infer Result<&str, NoneError>? What remaining issues would there be- probably finding a common error type for ?s to convert to? (Have I missed discussion of this somewhere?)

@shepmaster I agree with the type inference. Funnily enough, though, I tried your exact code with a somewhat naive try_ implementation and it works fine: https://github.com/norcalli/koption_macros/blob/4362fba8fa9b6c62fdaef4df30060234381141e7/src/lib.rs#L23

    let x = try_! { config.log?.level? }.unwrap_or("foo".to_owned());
    assert_eq!(x, "debug");

works just fine.

a somewhat naive try_ implementation

Yes, but your macro invocation returns a String, not a &str, requiring ownership. You don't show the surrounding code, but this will fail because we don't have ownership of the Config:

fn example(config: &Config) {
    let x = try_! { config.log?.level? }.unwrap_or_else(|| String::from("foo"));
}
error[E0507]: cannot move out of captured variable in an `Fn` closure
  --> src/lib.rs:20:21
   |
19 | fn example(config: &Config) {
   |            ------ captured outer variable
20 |     let x = try_! { config.log?.level? }.unwrap_or_else(|| String::from("foo"));
   |                     ^^^^^^^^^^ cannot move out of captured variable in an `Fn` closure

It also unconditionally allocates a String; I used unwrap_or_else in this example to avoid that inefficiency.

It is a shame this feature was not stabilized before async/await blocks. IMO it would have been more consistent to have

let fut = async try {
    fut1().await?;
    fut2().await?;
    Ok(())
};

instead of allowing it to work without the try. But I suppose that ship has long since sailed.

Re: auto-wrapping, I don't think it will be possible to be consistent with async blocks now. async blocks do "auto-wrapping" of a sort, into an anonymous type implementing Future. But this is true even with early returns, which would not be possible with try blocks.

It could become doubly confusing if we ever do have a hypothetical async try block. Should that auto-wrap the result?

I don't think we've lost any chances at consistency in the auto-wrapping sense. Both async blocks and functions can use ?, and both must do their own manual Ok-wrapping, for both early and final returns.

A try block, on the other hand, could use ? with automatic Ok-wrapping, including for "early returns" assuming an early return feature- perhaps label-break-value. A hypothetical try function could easily do automatic Ok-wrapping on both early and final returns.

A hypothetical async try block could simply combine the functionality of the two- auto-Ok-wrap, and then auto-Future-wrap. (The other way around is impossible to implement, and would arguably be written try async anyway.)

The inconsistency I do see is that we've conflated async blocks with functions. (This happened at the last minute contrary to the RFC, no less.) What this means is that return in async blocks exits the block, while return in try blocks exits the containing function. However, these do at least make sense in isolation, and async blocks without early-return or label-break-value would be much harder to use.

Anything stopping stabilization of this, or just nobody has taken the time to do it yet? I'm interested in creating the necessary PRs otherwise 🙂

On November 18, 2019 2:03:36 AM PST, Kampfkarren notifications@github.com wrote:

Anything stopping stabilization of this, or just nobody has taken the
time to do it yet? I'm interested in creating the necessary PRs
otherwise 🙂 >
>
-- >
You are receiving this because you were mentioned.>
Reply to this email directly or view it on GitHub:>
https://github.com/rust-lang/rust/issues/31436#issuecomment-554944079

Yes, the blocker to stabilization is working through the decision on Ok-wrapping. This shouldn't be stabilized until we have consensus on how it should behave.

I'm personally against Ok-wrapping, but I am wondering how hard it would be to add after-the-fact. Is no-ok-wrapping forward compatible with ok-wrapping?

I can imagine tricky cases such as the ambiguity of Result<Result<T,E>>, but in such ambiguous cases, we could just fall back to no-wrapping. The user could then explicitly Ok-wrap to disambiguate. That doesn't seem too bad, since I don't expect this sort ambiguity to come up too often...

There should be no ambiguity at all, because it's not Ok-coercion but Ok-wrapping. try { ...; x } would yield Ok(x) just as unambiguously as Ok({ ...; x }).

@joshtriplett Is this unresolved? The tracking issue has resolve whether catch blocks should "wrap" result value as checked off, citing https://github.com/rust-lang/rust/issues/41414

@rpjohnst Sorry, I should have been more clear. What I mean is that if we stabilized try now without Ok-wrapping, I believe it could be added later backwards-compatibly.

That is, I think most people agree that we should have try blocks, but not everyone agrees about catch or ok-wrapping. But I don't think those discussions need to block try...

@Kampfkarren Yes. The above conversation details the progression of this matter. It was prematurely ticked off without fully consulting everyone. @joshtriplett in particular had concerns, which several others (including myself) shared.

@mark-i-m How exactly do you see Ok-wrapping being added in the future? I'm trying to figure out how that might be done, and I can't quite see it.

So I will preface this by saying I don't know if it's a good idea or not...

We would stabilize try block without ok-wrapping. For example:

let x: Result<usize, E> = try { 3 }; // Error: expected Result, found usize
let x: Result<usize, E> = try { Ok(3) }; // Ok (no pun intended)

Later, suppose that we came to consensus that we should have Ok-wrapping, then we could allow some cases that didn't previously work before:

let x: Result<usize, E> = try { 3 }; // Ok
let x: Result<usize, E> = try { Ok(3) }; // Also Ok for backwards compat
let x: Result<Result<usize, E1>, E2> = try { Ok(3) }; // Ok(Ok(3))
let x: Result<Result<usize, E1>, E2> = try { Ok(Ok(3)) }; // Ok(Ok(3))

The question is whether this can cause something to become ambiguous that wasn't before. For example:

let x = try { Err(3) }; // If x: Result<Result<T1, usize>, usize>, then it is not clear if user meant Ok(Err(3)) or Err(3)...

Although, maybe Ok-wrapping already has to grapple with this problem?

Anyway, my intuition is that such weird cases don't come up that often, so it may not matter that much.

What about using Ok-wrapping except where the returned type is Result or Option? That would allow simpler code in most cases but would allow specifying the exact value where needed.

// Ok-wrapped
let v: Result<i32, _> = try { 1 };

// not Ok-wrapped since the returned type is Result
let v: Result<i32, _> = try { Ok(1) };

// not Ok-wrapped since the returned type is Result
let v: Result<i32, _> = try { Err("error") };

// Ok-wrapped
let v: Option<i32> = try { 1 };

// not Ok-wrapped since the returned type is Option
let v: Option<i32> = try { Some(1) };

// not Ok-wrapped since the returned type is Option
let v: Option<i32> = try { None };

Adding Ok-coercion or some kind of syntax dependent Ok-wrapping (which is what would need to happen to support stabilising without it and introducing it later on) would be very bad for readability, and has been broadly argued against multiple times on i.rl.o (commonly by people misunderstanding the straightforward Ok-wrapping that is implemented).

I’m personally strongly in favour of Ok-wrapping as implemented, but would even more strongly be against any form of coercion or syntax dependence that makes understanding the situations which will wrap difficult (I would take having to write useless Ok(...)’s everywhere over having to try and figure out if it has coerced or not).

let x = try { Err(3) }; // If x: Result<Result<T1, usize>, usize>, then it is not clear if user meant Ok(Err(3)) or Err(3)...

Although, maybe Ok-wrapping already has to grapple with this problem?

Nope, that is unambiguously Ok(Err(3)), Ok-wrapping is independent of syntax or types, it just wraps whatever the output of the block is in the Try::Ok variant.

@mark-i-m I don't think we can reasonably move from one to the other after stabilization. As bad as I consider Ok-wrapping to be, inconsistent Ok-wrapping that tries to guess whether you want it or not would be even worse.

In my codebase I'm dealing with a lot of optional values, so I introduced my own try block like macro a long time ago. And back when I was introducing it I had various variants with and without Ok-wrapping, and the Ok-wrapping version turned out to be so much more ergonomic, that it is the sole macro that I ended up using.

I have a ton of optional values that I need to work with, and they are mostly numeric, so I have tons of situations like this:

let c = try { 2 * a? + b? };

Without Ok-wrapping this would be much less ergonomic to the point where I would likely stay on my own macro than using the real try blocks.

Given the venerable history of this tracking issue, its original and regrettable conflation with the ? operator, and the roadblock over the Ok-wrapping issue, I'd suggest closing this issue outright and sending try back to the beginning of the RFC process, where this discussion can get the visibility it deserves and (hopefully) reach some sort of conclusion.

Without Ok-wrapping this would be much less ergonomic

Could you please elaborate on what exactly non-ergonomic stuff it would introduce?

Without Ok-wrapping, your example would look like:

let c = try { Ok(2 * a? + b?) };

which is pretty good in my opinion.

I mean, with a small example like this it might look like overkill but the more code the try block contains the less impact this Ok(...) wrapper causes.

Further to @CreepySkeleton's comment, it should be noted that it is very easy to create a macro that emulates Ok-wrapping if the try block does not do it (and someone will surely create a standard crate for this tiny macro), but the converse is not so.

That macro is not possible while the Try trait is unstable.

Why? Anyway, when it does stabilise (hypothetically not too far in the future), it will be very much possible.

@Nemo157 try blocks are also on nightly only right now, and they likely won't be stabilized in the unlikely case that we decide to rip out Try. This means that they likely won't be stabilized before Try. So, saying the macro isn't possible doesn't makes sense.

@KrishnaSannasi I'm curious why Try might be ripped out?

@mark-i-m I don't think it will, I am just explaining why worrying about Try being on nightly wrt to try blocks is not a realistic concern. I am looking forward to Try on stable.

Given that ? has been stabilized already, and try blocks have a clear design encompassing both Result and Option in the same way that ? does, there's no reason I can see to block stabilizing them on stabilizing Try. I haven't been keeping a close eye on it, but my impressions were that there was much less consensus on the design of Try than for try blocks, so I could see try blocks stabilizing years before the Try trait (as has happened for ?). And even if the Try trait is abandoned I see no reason that should block try blocks being stabilized as working with just Result and Option like ? would then be.

(For _why_ you couldn't write that macro given stabilized try blocks and an unstable Try trait, the macro would have expand to try { Try::from_ok($expr) }; you could create per-type macros for just Result and Option, but IMO that would not meet the "very easy to [...] emulate" point).

Given ? is already special-cased stable even though the Try trait can't be used on stable, I don't see why Try being unstable would block try blocks being implemented on stable, because if Try trait is removed we still have Option and Result supporting ? on stable, just without the ergonomics.

I would suggest the following concept for try catch semantic ...

Consider the following code:

union SomeFunctionMultipleError {
    err0: Error1,
    err1: Error2,
}

struct SomeFunctionFnError {
    index: u32,
    errors: SomeFunctionMultipleError,
}

fn some_function() -> Result<i32, SomeFunctionFnError> {
    if 0 == 0 {
        Ok(2)
    } else {
        Err(SomeFunctionFnError{ index: 0, errors: SomeFunctionMultipleError {err1: Error2 {id0: 0, id1: 0, id3: 0}}})
    }
}

union OtherFunctionMultipleError {
    err0: Error1,
    err1: Error2,
    err2: Error3,
}

struct OtherFunctionFnError {
    id: u32,
    errors: OtherFunctionMultipleError,
}

fn other_function() -> Result<i32, OtherFunctionFnError> {
    if 0 == 0 {
        Ok(2)
    } else {
        Err(OtherFunctionFnError {id: 0, errors: OtherFunctionMultipleError {err0: Error1 {id0: 0, id1: 0}}})
    }
}

This is the code that could be generated by Zero-Overhead exceptions in Rust with following syntax feature:

fn some_function() -> i32 throws Error1, Error2 {
    if 0 == 0 {
        2
    } else {
        Error2 {id0: 0, id1: 0, id3: 0}.throw
    }
}

fn other_function() -> i32 throws Error1, Error2, Error3 {
    if 0 == 0 {
        2
    } else {
        Error1{id0: 0, id1: 0}.throw
    }
}

or even these errors could be deduced by compiler implicitly:

fn some_function(i: i32) -> i32 throws { // Implicitly throws Error1, Error2
    if i == 0 {
        2
    } else if i == 1 {
        Error1 {id0: 0, id1: 0, id3: 0}.throw
    } else {
        Error2 {id0: 0, id1: 0, id3: 0}.throw
    }
}

fn other_function(i: i32) -> i32 throws { // Implicitly throws Error1
    if i == 0 {
        2
    } else {
        Error1{id0: 0, id1: 0}.throw
    }
}

This nothing else syntactic sugar !! Behavior is the same !!

Hi all,

Have anybody view my proposal regarding Zero-overhead exceptions that is above ?

@redradist One of the main points of try block is that we could use it inside of a function, with no need to create a function for every block. Your proposal is entirely unrelated here as far as I see it.

Just today I felt the need for try blocks. I have a big function that has many ? operations. I wanted to add context to the errors, but doing so for each ? would've needed a huge amount of boilerplate. Catching the errors with try and adding the context in one place would have prevented this.

Btw. wrapping the operations in an inner function would have been hard in this case, because the context has not obvious lifetime, and dividing stuff into multiple functions breaks NLL.

I would like to mention that in the current implementation a try block is not an expression. Think that this is an oversight.

I would like to mention that in the current implementation a try block is not an expression. Think that this is an oversight.

Could you post the code that isn't working for you? I'm able to use it in an expression context here: (Rust Playground)

#![feature(try_blocks)]

fn main() {
    let s: Result<(), ()> = try { () };
}

Sure, here it is.

And here is another that shows that type inference on try blocks is not yet complete. Which is especially annoying since type ascriptions are not supported in if let blocks.

@Nokel81 the problem with your former example is that the expression in if let $pat = $expr is not a regular expression context, but rather a special "no brackets expression" context. For an example of how this works with struct expressions, see this example where it is syntactically clear that there is a struct expression there, and this example where it is not. So the error is not that try isn't an expression, but that the error is wrong, and should say "try expression is not allowed here; try surrounding it with parenthesis" (and the incorrect warning about unnecessary parenthesis suppressed).

Your latter example is actually ambiguous for type inference. The type of e is _: From<usize> in this case, which is not enough information to give it a concrete type. You would need to use it some manner to give it a concrete type to allow type inference to succeed. This is not an issue specific to try; it's how type inference works in Rust.

Now, if you immediately try to match as an Ok and discard the Err case, you have a case for a suboptimal error message with no real simple way to address it.

Ah thank you very much for the in-depth explanation. I guess I am still confused why the later to ambiguous for type inference. Why is the type of the expression not Result<isize, usize>?

The ? operator can perform type conversions between different error types using the From trait. It expands roughly to the following rust code (ignoring the Try trait):

match expr {
    Ok(v) => v,
    Err(e) => return From::from(e),
}

The From call uses the type of the final returned expression to determine what error type conversions to do, and won't default to the type of the passed-in value automatically.

Apologies if this has been addressed already, but it seems strange to me that:

#![feature(try_blocks)]

fn main() -> Result<(), ()> {
    let result = try { // no type annotation
        Err(())?;
    };
    result.map_err(|err| err)
}

fails to compile with:

error[E0282]: type annotations needed

but:

#![feature(try_blocks)]

fn main() -> Result<(), ()> {
    let result : Result<_, _> = try { // partial type annotation
        Err(())?;
    };
    result.map_err(|err| err)
}

is okay.

If this were a problem because the type arguments of Result could not be deduced I'd understand, but, as shown above, that's not the case and rustc is able to perform inference once it is told that the result of a try expression is some sort of Result, which it ought to be able to infer from core::ops::Try::into_result.

Thoughts?

@nwsharp that's because try/? is generic over Try types. If you had some other type that was impl Try<Ok=_, Error=()>, the try block could evaluate to that type as well as Result. Desugared, your example is roughly

#![feature(try_trait, label_break_value)]

use std::ops::Try;

fn main() -> Result<(), ()> {
    let result /*: Result<_, _>*/ = 'block: {
        match Try::into_result(Err(())) {
            Ok(ok) => Try::from_ok(ok),
            Err(err) => {
                break 'block Try::from_error(From::from(err));
            }
        }
    };
    result.map_err(|err| err)
}

@CAD97 Thank you for the explanation.

That said, I didn't expect that try would effectively be capable of causing a sort-of conversion between different Try impls.

I'd expect a de-surgaring where the same Try impl is selected for into_result, from_ok, and from_error.

In my opinion, the ergonomic loss of being unable to perform the type inference (especially considering that no alternate impl Try even exists), doesn't outweigh the benefit of allowing this conversion.

We could permit inference by removing the ambiguity, and keep ability to opt-in to the conversion via something like:

try { ... }.into()

With the corresponding blanket impl:

impl<T: Try, E: Into<T::Err>> From<Result<T::Ok, E>> for T {
    fn from(result: Result<T::Ok, E>) -> Self {
        match result {
            Ok(ok) => T::from_ok(ok),
            Err(err) => T::from_err(err.into()),
        }
    }
}

(Which honestly I suppose makes sense having regardless, though I do personally doubt the automatic conversion of error types here. If it's wanted, the user should .map_err() on the Result.)

In general, I think that this de-sugaring is "too clever". It hides too much and its current semantics are liable to confuse people. (Especially considering that the current implementation asks for type annotations on something that doesn't support them directly!)

Or, going even further with the blanket impl, I suppose.

impl <T: Try, U: Try> From<U> for T 
    where U::Ok : Into<T::Ok>, U::Err : Into<T::Err>
{
    fn from(other: U) -> Self {
        match other.into_result() {
            Ok(ok) => Self::from_ok(ok.into()),
            Err(err) => Self::from_err(err.into()),
        }
    }
}

Or whatever...

That said, I didn't expect that try would effectively be capable of causing a sort-of conversion between different Try impls.

I'd expect a de-surgaring where the same Try impl is selected for into_result, from_ok, and from_error.

In my opinion, the ergonomic loss of being unable to perform the type inference (especially considering that no alternate impl Try even exists), doesn't outweigh the benefit of allowing this conversion.

There are four stable Try types: Option<T>, Result<T, E>, Poll<Result<T, E>>, and Poll<Option<Result<T, E>>.

NoneError is unstable, so Option<T> is stuck trying in Option<T> while NoneError is unstable. (Note though that the docs explicitly call out From<NoneError> as "enable option? to your error type.")

The Poll impls, however, set their error type to E. Because of this, the "type morphing" of Try is stable, because you can ? a Poll<Result<T, E>> in a -> Result<_, E> to get a Poll<T> and early return the E case.

In fact, this powers a "cute" little helper:

fn lift_err<T, E>(x: Poll<Result<T, E>>) -> Result<Poll<T>, E> { Ok(x?) }

@CAD97 Thanks for humoring me. This will be a tricky thing to teach to newcomers and will require quite a bit of love in terms of error messages.

Has thought been given to allowing the specification of the desired impl Try to assuage the unintuitive behavior here?

For example, bikeshedding for a bit, try<T> { ... }. Or is there yet again something to trip up on with even that?

To maybe add a bit more color here, the fact that try { } over a bunch of Result's doesn't "just" produce a Result is unexpected and makes me sad. I understand why, but I don't like it.

Yes, there has been discussion of the combination of "generalized type ascription" (there's your term to search for) and try. I think, last I heard, try: Result<_, _> { .. } was intended to work eventually.

But I do agree with you: try blocks deserve some targeted diagnostics for making sure their output type is specified.

Please see this separate issue for a specific narrow question to resolve language team consensus on the matter of Ok-wrapping.

Please read the opening comment of that thread before commenting, and in particular, please note that that thread is only about that one question, not any other issue related to try or ? or Try.

I don't see why the try block is needed. This syntax

fn main() -> Result<(), ()> {
    try {
        if foo() {
            Err(())?
        }
        ()
    }
}

can be replaced with this:

fn main() -> Result<(), ()> {
    Ok({
        if foo() {
            Err(())?
        }
        ()
    })
}

They both use the same number of characters, but the second is already stable.

When assigning the result to a variable, that might indicate a helper function should be created to return the result. If that's not possible, a closure can be used instead.

@dylni try blocks are especially useful when they don’t contain the entire body of a function. The ? operator on error makes flow control go to the end of the inner-most try block, without returning from the function.

```rust
fn main() /* no result here */ {
let result = try {
foo()?.bar()?.baz()?
};
match result {
// …
}
}

@SimonSapin Does that come up that often? I've rarely had a situation it would make sense for, and there's usually a good way to get around it. In your example:

fn main() /* no result here */ {
    let result  = foo()
        .and_then(|x| x.bar())
        .and_then(|x| x.baz());
    match result {
        // …
    }
}

That's more verbose, but I think a simpler solution would be a method closure syntax:

fn main() /* no result here */ {
    let result  = foo()
        .and_then(::bar)
        .and_then(::baz);
    match result {
        // …
    }
}

The type is also correctly inferred with and_then, where you need type annotations for try. I've had this come up infrequently enough that I don't think a terser syntax would be worth the readability harm.

The accepted RFC has some more reasoning: https://rust-lang.github.io/rfcs/0243-trait-based-exception-handling.html

Anyway the arguments in favor of language constructs for control flow with the ? (and .await) operator over chaining methods like and_then have already been discussed extensively.

Anyway the arguments in favor of language constructs for control flow with the ? (and .await) operator over chaining methods like and_then have already been discussed extensively.

@SimonSapin Thanks. This and re-reading the RFC convinced me this can be useful.

I thought I could use try blocks to easily add context to errors, but no luck so far.

I wrote a small function that works fine. Notice that File::open()? fails with a std::io::Error while the following line fails with an anyhow::Error. Despite the differing types, the compiler figures out how to convert both to a Result<_, anyhow::Error>.

fn tls_add_cert(config: &ClientConfig, path: impl AsRef<Path>) -> Result<(usize, usize), anyhow::Error> {
    let path = path.as_ref();
    let mut file = BufReader::new(File::open(path)?);
    Ok(config.root_store.add_pem_file(&mut file)
        .map_err(|_| anyhow!("Bad PEM file"))?)
}

I wanted to add some error context so I tried to use a try-block and anyhow's with_context():

fn tls_add_cert(config: &ClientConfig, path: impl AsRef<Path>) -> anyhow::Result<(usize, usize)> {
    let path = path.as_ref();
    try {
        let mut file = BufReader::new(File::open(path)?);
        Ok(config.root_store.add_pem_file(&mut file)
            .map_err(|_| anyhow!("Bad PEM file"))?)
    }
    .with_context(|| format!("Error adding certificate {}", path.display()))
}

But now type inference fails:

error[E0282]: type annotations needed
  --> src/net.rs:29:5
   |
29 | /     try {
30 | |         let mut file = BufReader::new(File::open(path)?);
31 | |         Ok(config.root_store.add_pem_file(&mut file)
32 | |             .map_err(|_| anyhow!("Bad PEM file"))?)
33 | |     }
   | |_____^ cannot infer type
   |
   = note: type must be known at this point
   ```

I don't understand why a type annotation is needed here but not in the first case. Nor do I see any easy way to add one, as opposed to using an [IIFE](https://en.wikipedia.org/wiki/Immediately_invoked_function_expression) which does let me add an annotation:

```rust
(|| -> Result<_, anyhow::Error> {
    let domain = DNSNameRef::try_from_ascii_str(host)?;
    let tcp = TcpStream::connect(&(host, port)).await?;

    Ok(tls.connect(domain, tcp).await?)
})()
.with_context(|| format!("Error connecting to {}:{}", host, port))

@jkugelman

Again,

that's because try/? is generic over Try types. If you had some other type that was [impl Try<Ok=_, Error=anyhow::Error>], the try block could evaluate to that type as well as Result.

(Also, you don't need to Ok your trailing expression in a try block (#70941).)

I think the fact that this continues to show up means that

  • Before stabilization, try needs to support a type ascription (try: Result<_,_> { or whatever) or otherwise mitigate this issue,
  • This definitely needs targeted diagnostics for when type inference of a try block fails, and
  • We should strongly consider giving try a type fallback to Result<_,_> when it's not otherwise constrained. Yes, that's difficult, under-specified, and potentially problematic, but it _would_ solve the 80% case of try blocks needed a type annotation because of $12: Try<Ok=$5, Error=$8> not being specific enough.

Additionally, given #70941 seems to be resolving towards "yes, we want (some form of) 'Try::from_ok wrapping'", we probably _also_ want a targeted diagnostic for when the tail expression of a try block is returning Ok(x) when x would work.

I suspect that the right behavior for try is

  • extend the syntax to permit a manual ascription like try: Result<_, _> { .. }, try as Result<>, or whatever (I think try: Result is probably fine? it seems to be the preferred syntax)
  • examine the "expected type" that comes from context -- if one is present, prefer that as the result type of a try
  • otherwise, default to Result<_, _> -- this is not type inference fallback like with i32, it would happen earlier, but that would mean that things like try { }.with_context(...) compile.

However, I am concerned that we may get errors around ? and the into coercion, at least so long as the error type is not specified. In particular if you write code where you ? the result of a try block, like so:

#![feature(try_blocks)]

use std::error::Error;
fn foo() -> Result<(), Box<dyn Error>> {
    let x: Result<_, _> = try {
        std::fs::File::open("foo")?;
    };

    x?;

    Ok(())
}

fn main() { 
}

You still get errors (playground) and rightly so, because it's not clear on which ? the "into" coercion should trigger.

I'm not sure what's the best solution here but it probably involves some type inference fallback that will make me nervous.

probably involves some type inference fallback that will make me nervous.

Simplest one: if all uses of ? in a given Try block contain the same Try::Error type, use that error type for the containing try block (unless otherwise bound).

The "(unless otherwise bound)" is, of course, the subtle scary part.

I hope I'm not being too unconstructive with this post. However, I wanted to contrast @nikomatsakis example with one from a parallel world where ? doesn't do a forced conversion, and there is no auto-wrapping of the try block result:

use std::error::Error;
fn foo() -> Result<(), Box<dyn Error>> {
    let x = try {
        std::fs::File::open("foo").err_convert()?;
        Ok(())
    };

    x?;

    Ok(())
}

In this world:

  • It's easy to see that both the try scope and the fn itself result in success without any value. It's also easy to see without even trying that they produce Results.
  • It's obvious where error conversion happens.
  • The error conversion could be moved to the x? expression, making the try scope specific to the std::fs::File operations.
  • All type hints chain fluently from the type signature. Both for the machine and for us humans.
  • Type hinting by the user is only required in cases where we actually want to fold errors into another, independent one.

I would be very happy in that parallel universe.

@phaylon While I appreciate the careful way you wrote that comment, I'm afraid it is rather unconstructive. Error conversion is part of ? and that is not going to change, and, in light of that, ok-wrapping is basically orthogonal from the rest of this discussion.

If try functions (with return and throw types) are ever to be considered, then maybe it would be worth to also consider the syntax for ascribing try block to be something similar.

e.g.

try fn foo() -> u32 throw String {
  let result = try: u32 throw String {
    123
  };
  result?
}

Sorry if this has been discussed but what are the advantages of using

try fn foo() -> u32 throw String { ... }

or similar as opposed to

fn foo() -> Result<u32, String> { ... }

?
Just seems like duplicate syntax.

@gorilskij As I understand it, the primary advantage is to get Ok-wrapping. Otherwise, you have to write:

fn foo() -> Result<u32, String> {
    try {
        // function body
    }
}

Some people also prefer throws as they find the exceptions terminology relatable.

Personally, I want to stay as far as possible from the appearance of exceptions.

This is not the thread to discuss try fn, so please don't take this tangent further. This thread is for the accepted feature of try blocks, not the potential (and as of yet, not RFCd) feature try fn.

I just noticed that the original RFC for ? proposed using Into, not From. It states:

The present RFC uses the std::convert::Into trait for this purpose (which has a blanket impl forwarding from From).

Though leaves the exact upcasting method as an unresolved question. Into was (presumably) preferred based on the guidance from From:

Prefer using Into over using From when specifying trait bounds on a generic function. This way, types that directly implement Into can be used as arguments as well.

However, in the Try trait RFC, Into is no longer mentioned, and the conversion is done using From instead. This is also what the code now uses, even for ?
https://github.com/rust-lang/rust/blob/b613c989594f1cbf0d4af1a7a153786cca7792c8/src/librustc_ast_lowering/expr.rs#L1232

This seems unfortunate, as there is no blanket implementation going from Into to From. This means that errors that implement Into (as opposed to From) either in error, for legacy reasons, or out of some other need, cannot be used with ? (or Try). It also means that any implementation that follows the recommendation in the standard library to use Into in bounds cannot use ?. With an example, the standard library recommends I write:

fn with_user_err<E>(op: impl Fn() -> Result<(), E>) -> Result<(), MyError>
where E: Into<MyError>

but if I do, I cannot use ? in the function body. If I want to do that, I have to write

fn with_user_err<E>(op: impl Fn() -> Result<(), E>) -> Result<(), MyError>
where MyError: From<E>

but if I do, users with error types that implement Into instead of From cannot use this function. Note that the inverse is not true, due to the blanket impl of Into based on From.

It is probably (?) too late to fix ? now (which is _very_ unfortunate — maybe in the next edition?), but we should at least make sure to not dig deeper down that path in the Try trait.

@jonhoo @cuviper tried to change the desugaring from From to Into in #60796 to check #38751 and it resulted in a large amount of inference breakage exactly because of the From -> Into blanket impl made it harder for rustc to handle the common case of the identity conversion. It was decided that it wasn't worth the cost breaking inference that much.

@jonhoo you might also find this comment from niko informative:

There is also a hard-coded limit in the trait system. If you have to solve a goal like ?X: Into<ReturnType>, we will fail to resolve, but if you have to solve a goal like ReturnType: From<?X>, we will potentially succeed and infer a value for ?X.

Edit: Here, ?X refers to some unknown inference variable. The hard-coded limit in today's trait system is that the Self type must be at least partly inferred for us to explore that option.

The TL;DR is that inferring with Into is harder, and in an inherent way to the way in which the trait solver functions.

@KrishnaSannasi @CAD97 Thank you, that's helpful! I still worry about us stabilizing too much that's based on From given that we are leaving implementors of Into sort of permanently out. Is the expectation that the inference here may eventually get better? Should the guidance to prefer Into in bounds be changed? Are we expecting that with the new trait coherence rules in 1.41 (I think it was) there is no longer a reason to implement only Into, and consider all of those impls bugs?

If inference get's good enough, it should be forward compatible to change to Into later. The worst that we could break is inference given that From implies Into

Does this (the Try trait) cover allowing ? to work with the Into/From traits with generic bounds for types which implement Try (e.g. Result itself)?

i.e. ? in a closure or function which returns e.g. impl Into<Result>

(It doesn't seem to when I try that on nightly?)

I'm keen to see this stabilised. Having read this thread, and #70941, I think the summary should be updated as follows:

  1. "resolve whether catch blocks should "wrap" result value" should be ticked, "Resolved as yes"

  2. New concern added about these inference problems. Perhaps something like:

    • [ ] Ergonomic difficulties due to problems with type inference.

ISTM that this last concern could be addressed by, amongst other ways:

(Some of these options are not mutually exclusive.)

Thanks for your attention and I hope you find this message helpful.

Type ascription has a number of issues (syntactic and otherwise), and seems unlikely to get implemented soon, let alone stabilized; blocking try blocks on type ascription syntax doesn't seem appropriate.

A fallback to Result might help, but doesn't solve the type inference problems with error types: try { expr? }? (or in practice more complex equivalents) have effectively two calls to .into(), which gives the compiler too much flexibility on the intermediate type.

@ijackson thanks for taking the initiative to summarize the current state. I think you're correct that there are various ways we could improve on try blocks, but one of the problems is that we're not sure which one to do, in part because each solution has its own unique drawbacks.

With regard to type ascription, though, I do feel like the implementation challenges there aren't that difficult. That might be a good candidate to put some attention into and try to push it over the finish line regardless. I don't recall whether there was much controversy about the syntax or anything like that.

On Wed, Aug 05, 2020 at 02:29:06PM -0700, Niko Matsakis wrote:

With regard to type ascription, though, I do feel like the implementation challenges there aren't that difficult. That might be a good candidate to put some attention into and try to push it over the finish line regardless. I don't recall whether there was much controversy about the syntax or anything like that.

As I recall, the primary concern was that allowing type ascription
everywhere would be a substantial grammar change, and potentially a
limiting one. I don't recall the full details, just that the concern was
raised.

Personally I think the ergonomic problems are not so bad that it is not worth stabilising this feature now. Even without expression type ascription, introducing a let binding is not so ugly a workaround.

Also, try blocks might be useful in macros. In particular, I wonder if @withoutboats excellent fehler library would suffer from fewer problems with deficiencies in our macro system, if it could wrap the bodies of procs in try.

I run into places where I would love to use try blocks a lot. It would be good to get this over the line. Personally, I would absolutely, 100% sacrifice type ascription if it were required to get try blocks over the line. I have yet to find myself in a situation where I said "dang I would love to have type ascription here," but end up doing an IIFE to simulate try blocks a lot. Leaving a long-term, useful feature unstable because it conflicts with another long-term, unstable feature is a really unfortunate situation.

To be slightly more specific, I find myself doing this when I am inside a function that returns Result, but I want to do some sort of processing on things that return an Option. That said, if Try in general were stable, I would still probably prefer try blocks, as I don't actually want to return from the main function to do so, but instead, give some sort of default value if anything in the chain is None. This tends to happen for me in serialization style code.

Personally, I've wanted type ascription far more often than try blocks (though i have wanted both at times). In particular, I've often struggled with "type debugging" where the compiler infers a different type from what i expected. Usually, you have to add a new let binding somewhere, which is really disruptive and causes rustfmt to break the undo history. Moreover, there are lots of places where type ascription would avoid an extra turbo fish.

In constrast, I can just use and_then or other combinators to terminate early without exiting. Perhaps not as clean aa try blocks, but not that bad either.

@steveklabnik @mark-i-m Try blocks and type ascription are not in any way in conflict, it is not a question of one feature or another. It's just try blocks have unergonomic type inference failures, and generalized type ascription could be a way to solve that problem, but since generalized type ascription is not a near term feature or even a sure thing, @joshtriplett (and I agree) doesn't want this feature to block on generalized type ascription happening.

This doesn't even mean we wouldn't make generalized type ascription the solution to the problem; one option deserving investigation is "stabilize try as is, expecting that someday generalized type ascription will solve that problem." All that's been said is don't block stabilization on type ascription.


@rust-lang/lang I have to admit its a bit hard to understand the nuance of the type inference failure from this thread, because of the limitations of GitHub and the many other subjects that are discussed here. Since reaching a decision about how to handle the inference failures is the only thing blocking try blocks from stabilizing, I think it would be beneficial if we had a meeting to discuss this, and someone could take point on getting a deep understanding of the inference issue.

One question that occurs to me, for example: is this inference issue specifically because of the conversion flexibility we've allowed in Try? I know that decision has been discussed to death, but if that's so this seems like pertinent new information that could justify changing the definition of the Try trait.

@withoutboats I agree with the need to collect all the information in one place and the desire to push this feature over the finish line. That said, I think the last time we investigated here it also became clear that changes to Try might be difficult because of backwards compatibility -- @cramertj mentioned some specific Pin impls, IIRC.

Was this page helpful?
0 / 5 - 0 ratings