Feature request: Allow explicit <return type> on try {} blocks

I heard the try {} block syntax was now available experimentally in Nightly. I am very excited about this. The main reason I am excited is I expect it to mitigate frustrations where Rust does not currently allow mixing ? types (Option, Result, different types of Result) within a single function body.

I do notice one thing, which is that there is no way for a try body to explicitly declare its return type. Consider this example, which I tested on rustc 1.81.0-nightly (c1b336cb6 2024-06-21):

#![feature(try_blocks)]

fn main() {
    let x = try {
        let a : std::io::Result<()> = Ok(());
        let b : std::fmt::Result = Ok(());
        let c = a?;
        let d = b?;
    };

    println!("Hello, world!");
}

This fails with the error:

error[E0282]: type annotations needed
 --> src/main.rs:4:9
  |
4 |     let x = try {
  |         ^
  |
help: consider giving `x` an explicit type
  |
4 |     let x: /* Type */ = try {
  |          ++++++++++++

For more information about this error, try `rustc --explain E0282`.
error: could not compile `deletemetry` (bin "deletemetry") due to 1 previous error

I would like to propose that a <type> be allowed on the try {} block, like:

#![feature(try_blocks)]

fn main() {
    let x = try<std::io::Result<()> {
        let a : std::io::Result<()> = Ok(());
        let b : std::fmt::Result = Ok(());
        let c = a?;
        let d = b?;
    };

    println!("Hello, world!");
}

At present, this fails with

   Compiling deletemetry v0.1.0 (/home/mcc/work/temp/deletemetry)
error: expected expression, found reserved keyword `try`
 --> src/main.rs:4:13
  |
4 |     let x = try<std::io::Result<()> {
  |             ^^^ expected expression

error: could not compile `deletemetry` (bin "deletemetry") due to 1 previous error

I think this would be consistent with other typed structures. The main benefit I foresee is better error messages. I anticipate people would use this explicit-try-type feature relatively rarely, but in unusual situations and when inference is making bad guesses it would be helpful.

Forseeing potential objections:

Why not just type the variable being assigned to, as the error help suggests?

This example is designed to be minimal. I can imagine scenarios where typing the assignment is not an option, such as passing the result of a try {} to a function, returning it from a closure, or using try {}.into().

Why not just get better inference?

In principle, the Rust compiler could have inferred a type for the try {} expression, for example guessing the first ? type, std::io::Result<()>, as the correct one. (Right now there seems to be no inference on try {}s at all; if I comment out the let b and let d statements above I currently get the same error.) However, inference will inherently struggle in the case where the code itself is wrong (as my sample code above indeed does seem to be). Allowing me as the programmer to explicitly state my expected return value for the try {} block will give me the most useful error messages in a situation where I have given the compiler gibberish and therefore it is inferring the wrong type.

For example, in the case of my second example block above, my "expected" result is that I would get an error message on the line let d = b?;, as this is the line which violates the stated return value. This would have been a more useful error message than the try block as a whole simply telling me "I can't figure out what type you wanted here".

Why not use the syntax try -> std::io::Result<()>?

I have no strong opinion on the exact syntax for this feature, I just want a way to do it. (In testing I did try try -> std::io::Result<()> and try : std::io::Result<()> in case there was already a syntax for this and I'd guessed it wrong, but these were not recognized by the compiler.)

Am I posting this in the correct place?

I initially attempted to file this as a bug on Github but it said feature requests should go here. I think there is a tracking issue for this feature but it seems to be kind of a forest of sub-tracking-issues, so I do not know how to check if the feature I am requesting here is already planned.

2 Likes

This is not a problem with inference, but ambiguity. There are types for x for which your code above will compile (ignoring the trailing Ok(());, which is useless and causes a compilation error by itself). For example annotating x: Result<(), Box<dyn std::error::Error>> will make it work, but so will x: anyhow::Result<()>.

What is needed is some rule that makes one type preferred over the other, while keeping this type inferrable both by the compiler and by human intuition.

2 Likes

Rather than reverse-engineering what intuition the compiler expects me to have, I would prefer to have the option of just telling the compiler what I want to happen.


EDIT: "(ignoring the trailing Ok(());, which is useless and causes a compilation error by itself)" This is correct, for the avoidance of confusion I have edited the examples above to remove this.

You can use std::convert::identity::<TargetType>(try { .. }) as a form of inline ascription.

If this gets a sugar, it should be applicable to other types of blocks like const { }.

2 Likes

“Address issues with type inference” is (almost) the only unresolved issue left in the try block tracking issue that you linked. I think much of the related discussion is in the comment thread on that issue (which unfortunately is past the length where GitHub discussions become extremely annoying to load and search).

This draft RFC by @scottmcm was linked there, and this related Zulip thread. I believe a solution is strongly desired but there isn’t yet consensus on a concrete plan.

4 Likes

isn't this solved by type ascription?

also, i believe the ideomatic workaround is to use superfish to annotate the final Ok expression.

i've used the latter a fair bit when working with closures that return Result (expecially with the try_* methods in itertools)

also, i believe the ideomatic workaround is to use superfish to annotate the final Ok expression.

As SkiFire13 has pointed out, I think the try {} syntax as currently implemented doesn't end with an Ok()? In my testing this works:

let x:std::io::Result<i32> = try { 3 };
println!("Hello, world! {x:?}");

But this fails with a type error:

let x:std::io::Result<i32> = try { Ok(3) };
println!("Hello, world! {x:?}");

personally that seems kinda at odds with the rest of the language? like, it's kinda convienent not having to write it, but it would be much more convenient to not have to write Ok(()) at the end of every fallible function. i guess that means try blocks are also blocked (heh) on FromResidual being stabalized?

1 Like

In order to be a solution, something has to not create more problems than it solves. :slight_smile:

oops, i didn't realize ascription has been withdrawn. the De-RFC withdrawing it seems like a good candidate for appendix Z of the rustc dev guide too.

1 Like

And async {} block. I often wanted it to use ? within async blocks.

6 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.