# Pre-RFC: Another attempt to re-design the Try trait - Try::Kind and TryAs

**URL:** <https://internals.rust-lang.org/t/pre-rfc-another-attempt-to-re-design-the-try-trait-try-kind-and-tryas/24618>\
**Category:** language design\
**Created:** [September 25, 2026, 8:07pm UTC](https://internals.rust-lang.org/t/pre-rfc-another-attempt-to-re-design-the-try-trait-try-kind-and-tryas/24618 "2026-09-25T20:07:48Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![camsteffen](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/camsteffen/32/3246_2.png) [@camsteffen](https://internals.rust-lang.org/u/camsteffen)\
**Post date:** [September 25, 2026, 8:07pm UTC](https://internals.rust-lang.org/t/pre-rfc-another-attempt-to-re-design-the-try-trait-try-kind-and-tryas/24618/1 "2026-09-25T20:07:48Z")

</div>

This is an attempt to re-design the `Try` and `Residual` traits to be simpler while generally maintaining functionality. I call it "fat try" because it stuffs most everything into `Try` instead of breaking it up into several traits.

I believe this is workable solution but the "ControlFlow Wrinkle" below is one non-trivial obstacle. I have a local branch as a POC where I have been able to prove that this generally works, including changes to try desugaring.

* * *

Here are the traits. There's only two:

```rust
pub trait Try {
    type Kind;

    type Continue;

    type Break;

    fn from_continue(c: Self::Continue) -> Self;

    fn from_break(b: Self::Break) -> Self;

    fn branch(self) -> ControlFlow<Self::Break, Self::Continue>;
}

pub trait TryAs<T>: Try {
    type Try: Try<
	    Kind = Self::Kind,
	    Break = Self::Break,
	    Continue = T,
    >;
}

```

Example implementation:

```rust
impl<T, E> ops::Try for Result<T, E> {
    type Kind = Result<!, !>;
    type Continue = T;
    type Break = E;

    fn from_continue(t: T) -> Self {
        Ok(t)
    }

    fn from_break(e: E) -> Self {
        Err(e)
    }

    fn branch(self) -> ControlFlow<E, T> {
        match self {
            Err(e) => ControlFlow::Break(e),
            Ok(v) => ControlFlow::Continue(v),
        }
    }
}

impl<T, U, E> TryAs<U> for Result<T, E> {
    type Try = Result<U, E>;
}

```

For `Option`, `Kind` is `Option<!>` and `Break` is `()`.

## Try::Kind

(This is the main thing that makes this proposal different.)

**The `Try::Kind` Rule:** All `?` operators must have a target whose `Try::Kind` is equal to the `Try::Kind` of the result of the surrounding try scope (function or try block).

`Try::Kind` is a marker type (never constructed at runtime) which ascribes a _semantic_ to the `?` operator for the implemented type. The `Try::Kind` for `Result` and `Option` are `Result<!, !>` and `Option<!>` respectively. You can think of it as an answer to the question: "What does the `?` operator _test_ for this type?". `Try::Kind=Result<!, !>` means "`?` tests for the presence of an error". And `Try::Kind=Option<!>` means "`?` tests for the absence of a value". Note that the `Try::Kind` does _not_ necessarily indicate the actual type. For example, you may have `CustomError as Try<Kind=Result<!, !>>`.

## Custom Try Types

When implementing a custom `Try` type, you will need to decide what the `Kind` will be, and whether to re-use a std `Try::Kind` (`Result<!, !>` or `Option<!>`) or define your own. Perhaps the most common choice would be to use `Result<!, !>`, thus declaring your type to be a "custom result", and having the same semantic of "`?` tests the presence of an error". Or you may define a new `Try::Kind` to signify some domain-specific semantic. But then your type cannot be used in try scopes which produce a std type. Choosing a std `Try::Kind` gives you interoperability with the corresponding std type "for free". That is, you may use `?` on your custom error in a try scope that returns `Result` and visa versa.

## `?` uses `From`

The `?` operator includes a `From` conversion to convert the `?` target's `Break` type into the try scope output's `Try::Break` type.

So we can say that these are the full requirements for using `?`:

- The `Try::Kind` rule above
- The `Try::Break` of the `?` target must convert to the `Try::Break` of the try scope result using `From`.

`From` conversion on the `Break` value is **not** included in homogeneous try blocks. More on that later.

## The `TryAs` Trait

The `TryAs` trait draws a relationship between `Try` types that are the same type but with the `Continue` type interchanged. For example, `io::Result<u32> as TryAs<String>::Try = io::Result<String>`. Note that a `TryAs` conversion maintains that `Try::Kind` and `Try::Break` are unchanged. Also note that `TryAs` should maintain the same outer type, so it is more constrained than `Try::Kind`.

The `TryAs` trait is useful in function signatures that are generic over `Try`, and where the `Continue` type is changed. For example, here is a function signature for `std::array::try_from_fn` (some details omitted):

```rust
pub fn try_from_fn<T, R, const N: usize, F>(f: F) -> <R as TryAs<[T; N]>>::Try
where
    F: FnMut(usize) -> R,
    R: Try<Continue = T> + TryAs<[T; N]>

```

## Homogeneous Try Blocks

Homogeneous try blocks generally work as described in the RFC. The underlying type requirements just look different. Where heterogeneous try scopes use `Try::Kind` to enforce a relationship between types, homogeneous try blocks use `TryAs` instead.

**The Homogeneous `TryAs` Rule:** For every `?` operator within a homogeneous try block, given the `?` target is `T` and the try scope result is `R`, `<T as TryAs<<R as Try>::Continue>> = R`. In other words, the `?` target type can be related to the try scope result type via `TryAs`.

## `ControlFlow` Wrinkle

`ControlFlow` is stabilized with no `From` conversion in its `?` support. I don't know how to resolve this unfortunately. My preference is to use a time machine to reverse the stabilization, but I am unable to supply a time machine at this time.

## Comparison with `try_trait_v2`, discussion

- No "residual" types. The `Break` type in "fat try" is intended to be fully minimized and not carry any information about the originating `Try` type. The burden of defining which `Try` types are interoperable is fully shifted to the `Try` trait itself, particularly with `Try::Kind`. Also there is no more need "unwrap" the residual type in `from_residual`.
- No `FromResidual` implementations. With `Try::Kind`, all `Try` types of the same `Kind` are allowed to be used with each other (a `?` target paired with a try scope result) without implementing a trait for every pair within the set. You only need to implement `From` to convert `Break` types as needed.
- No `Residual` trait. (Generally replaced with `TryAs`)
- Less fine control over which types are inter-convertible. I think this is totally fine. But this is putting some faith in `Try::Kind` that it will make sense across use cases.
- `From` conversion is built into `?`. With `try_trait_v2`, you need to include `From` conversion into `FromResidual` implementations if desired. I think the advent of homogeneous try blocks at least partially negates some of the motive to remove universal `From` support. And I fear that having both variable `From` _and_ two types of try blocks is too much complexity overall.
- `Try::Kind` is kind of strange as a "marker type". Though I believe this is just relocating the strangeness of "residual types" like `Result<!, E>` and giving it a more dedicated place.
- Addition of `TryAs`. This trait might feel a bit ad-hoc, admittedly. You don't need it until you try to use homogeneous blocks or certain functions that use it in the interface.
- `TryAs` implementations tend to have a lot of generic parameters and this can be confusing. I wonder if there is a way to auto-derive these based on the `Try` impl.
- `TryAs` for types with a non-generic `Continue` is possible but strange to implement.
- `TryAs` isn't strict enough - it should be required to be round-trippable: `<R as TryAs<R::Continue>>::Try = R`. This could be a lint.

---

<div class="post-metadata">

**Author:** ![MusicalNinjaDad](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/musicalninjadad/32/11986_2.png) [@MusicalNinjaDad](https://internals.rust-lang.org/u/MusicalNinjaDad)\
**Post date:** [September 27, 2026, 1:58pm UTC](https://internals.rust-lang.org/t/pre-rfc-another-attempt-to-re-design-the-try-trait-try-kind-and-tryas/24618/2 "2026-09-27T13:58:42Z")

</div>

Thanks for sharing. That helps to give a concrete idea of some of the trade-offs involved in simplification, which in turn will help with the discussions needed before stabilisation.

To really get a view on the trade-offs it'd be great to understand how other known use-cases are affected. My estimate, without having tried any of them, is:

- [ErrorCode](https://rust-lang.github.io/rfcs/3058-try-trait-v2.html#implementing-try-for-a-non-generic-type) would be OK, but interconversion between a potential `ErrorCodeU32` & `ErrorCodeI32` would not be possible without additional methods. (Here the Residual/Break types are different, not just the Output/Continue)
- [TracedResult](https://goals.rust-lang.org/2026/stabilize-try.html#capturing-error-context) would be possible, with the additional logic falling into `from_break`.
- [TracingResult](https://docs.rs/tracing-result/0.0.2/tracing_result/enum.TracingResult.html) would be possible, with logic in `branch` as today. A possible future move to a more generic `Tracing<Result>`, `Tracing<Option>`, etc. would be possible and could maybe even be more flexible by making use of `TryKind`
- [thread\_safely::Context](https://docs.rs/thread_safely/latest/thread_safely/) would not be possible, at least, not across different `TryKind`s
- A [homogeneous Poll](https://github.com/rust-lang/rust/issues/155368) would introduce a fourth `TryKind` in stdlib.
- In case of a homogeneous Poll, `??` and `???` etc. would need to be compiler-features, to allow for ergonomic approaches to the current Poll use-cases. Currently they (and other such wrapping-type cases) are possible with pure library implementation.
- A [semantically flattened OReO](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=812ff2d0a5b1c08bcef82f3c3d67c6e9) wouldn't allow "construction from parts via ?"

---

<div class="post-metadata">

**Author:** ![camsteffen](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/camsteffen/32/3246_2.png) [@camsteffen](https://internals.rust-lang.org/u/camsteffen)\
**Post date:** [September 27, 2026, 9:29pm UTC](https://internals.rust-lang.org/t/pre-rfc-another-attempt-to-re-design-the-try-trait-try-kind-and-tryas/24618/3 "2026-09-27T21:29:05Z")

</div>

These are great points for comparison, thanks!

> [@MusicalNinjaDad](#):
>
> - A [homogeneous Poll](https://github.com/rust-lang/rust/issues/155368) would introduce a fourth `TryKind` in stdlib.

Yes, and to me this feels like a really great case for `Try::Kind`. It has its own distinct semantic, and `Try::Kind` makes that distinction an explicit thing in code.

> [@MusicalNinjaDad](#):
>
> - In case of a homogeneous Poll, `??` and `???` etc. would need to be compiler-features, to allow for ergonomic approaches to the current Poll use-cases. Currently they (and other such wrapping-type cases) are possible with pure library implementation.

Could you elaborate on what you mean here? I think I'm missing some context.

> [@MusicalNinjaDad](#):
>
> - [thread\_safely::Context](https://docs.rs/thread_safely/latest/thread_safely/) would not be possible, at least, not across different `TryKind`s
> - ...
> - A [semantically flattened OReO](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=812ff2d0a5b1c08bcef82f3c3d67c6e9) wouldn't allow "construction from parts via ?"

These highlight the same limitation - the inability to coalesce multiple Try semantics in to a combined Try result. That does seem like it might be too severe of a limitation. Something else I'm noticing here is that `branch()` can't make much sense when you combine semantics like that. The whole point of `branch()` is that is splits over _one_ axis with _two_ possible outcomes.

We could have a `FromBreak` trait and give _that_ a `Kind` associated type. So then `Try::Kind` describes `branch()`'s semantic but `FromBreak::Kind` describes the semantic of the Try that Break'ed. Then we can support the OReO as well as `thread_safely::Context`. And `thread_safely::Context` would then be even more flexible than it was, supporting custom result types etc.

The ship is probably sailed for having `From` built into `?`, and for that reason we probably need `FromBreak` anyways.

As for "residual" vs "break" naming, I've actually come around to believe that "residual" is an okay name, but **only** in the case that it looks like e.g. `Result<!, E>`. If we can have "minimal break types", I think "residual" then becomes an unnecessary term. That was probably the general thinking behind it all along and I'm just catching up.

---

<div class="post-metadata">

**Author:** ![MusicalNinjaDad](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/musicalninjadad/32/11986_2.png) [@MusicalNinjaDad](https://internals.rust-lang.org/u/MusicalNinjaDad)\
**Post date:** [September 28, 2026, 8:23am UTC](https://internals.rust-lang.org/t/pre-rfc-another-attempt-to-re-design-the-try-trait-try-kind-and-tryas/24618/4 "2026-09-28T08:23:24Z")

</div>

> [@camsteffen](#):
>
> > [@MusicalNinjaDad](#):
> >
> > - In case of a homogeneous Poll, `??` and `???` etc. would need to be compiler-features, to allow for ergonomic approaches to the current Poll use-cases. Currently they (and other such wrapping-type cases) are possible with pure library implementation.
> 
> Could you elaborate on what you mean here? I think I'm missing some context.

Currently std has `impl<T, E, F: From<E>> FromResidual<Result<!, E>> for Poll<Result<T, F>>` which is designed to allow for the following shape:

```rust
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
    ...
}

fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
    ready!(self.poll_flush(cx))?; // <- Uses FromResidual<Result<!,E>> for Poll<Result<T,E>>
    Poll::Ready(Ok(()))
}

```

That's a very common use case which will require an ergonomic way to get from `Result<T,E>` to `Poll<Result<T,F>>`. With the current set of traits the above would continue to work, even with a homogenous `impl Try for Poll`, and could be adjusted to `self.poll_flush(cx)??` gradually over time. With a simplified set, I imagine the only fix would be to provide direct compiler support for desugaring multiple `?` where one Try wraps another, and force the shift from `ready!()?` to `??` with any change to Poll Try. (Technically possible, clear 3-way-trade-offs between Try, homogeneity & impact)

> [@camsteffen](#):
>
> > [@MusicalNinjaDad](#):
> >
> > - [thread\_safely::Context](https://docs.rs/thread_safely/latest/thread_safely/) would not be possible, at least, not across different `TryKind`s
> > - ...
> > - A [semantically flattened OReO](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=812ff2d0a5b1c08bcef82f3c3d67c6e9) wouldn't allow "construction from parts via ?"
> 
> These highlight the same limitation - the inability to coalesce multiple Try semantics in to a combined Try result. That does seem like it might be too severe of a limitation.

Yes, they are the same case for this particular discussion right now, although due to the significantly different semantics, I'd consider them separately both from a use-case-discussion perspective and technically (subtle differences)

As to whether it's too much of a limitation - I'd say: it's clearly one of the main trade-offs to consider.

> [@camsteffen](#):
>
> I'm noticing here is that `branch()` can't make much sense when you combine semantics like that. The whole point of `branch()` is that is splits over _one_ axis with _two_ possible outcomes.

I don't follow ... For both cases there is a clear-cut output & residual. For `Context`: this is purely semantic (has thread cancellation been requested?); for the "OReO" it is both sematic _and_ technical (did we get a data match?, if so - I want the data)

> [@camsteffen](#):
>
> We could have a `FromBreak` trait and give _that_ a `Kind` associated type.

Which brings us back very close to the current 3 trait setup. I can't remember from all the archaeology what the exact reason for `trait Residual` was rather than adding an associated type to `FromResidual`, or whether this was discussed (before my time) ...

> [@camsteffen](#):
>
> The ship is probably sailed for having `From` built into `?`, and for that reason we probably need `FromBreak` anyways.

No ships have sailed until stabilisation. But personally, I'd consider them to be semantically different enough to warrant separation, without even thinking about potential collisions in blanket implementations.

---

<div class="post-metadata">

**Author:** ![camsteffen](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/camsteffen/32/3246_2.png) [@camsteffen](https://internals.rust-lang.org/u/camsteffen)\
**Post date:** [September 28, 2026, 1:37pm UTC](https://internals.rust-lang.org/t/pre-rfc-another-attempt-to-re-design-the-try-trait-try-kind-and-tryas/24618/5 "2026-09-28T13:37:35Z")

</div>

> [@MusicalNinjaDad](#):
>
> > [@camsteffen](#):
> >
> > I'm noticing here is that `branch()` can't make much sense when you combine semantics like that. The whole point of `branch()` is that is splits over _one_ axis with _two_ possible outcomes.
> 
> I don't follow ... For both cases there is a clear-cut output & residual. For `Context`: this is purely semantic (has thread cancellation been requested?); for the "OReO" it is both sematic _and_ technical (did we get a data match?, if so - I want the data)

I guess I'm just noticing that the Continue/Break types don't neatly correspond to the branching semantic, unlike Result where the Continue/Break types neatly come from the two enum variants. But that's not really a problem other than that it might be a bit confusing. With the "OReO", it's like `branch()` is not testing for one thing but is testing for N specific things represented by the variants. But again, that's not really a problem and it ultimately works out fine.

> [@MusicalNinjaDad](#):
>
> > [@camsteffen](#):
> >
> > We could have a `FromBreak` trait and give _that_ a `Kind` associated type.
> 
> Which brings us back very close to the current 3 trait setup. I can't remember from all the archaeology what the exact reason for `trait Residual` was rather than adding an associated type to `FromResidual`, or whether this was discussed (before my time) ...

The `Residual` trait is explained pretty well in the [homogeneous blocks RFC](https://github.com/rust-lang/rfcs/blob/master/text/3721-homogeneous-try-blocks.md). The way I would explain it is that it has a dual purpose of 1) defining the type constraint for homogeneous blocks and 2) providing a way to define API's like `Iterator::try_find` or `array::try_from_fn`. My proposed `TryAs` pretty much serves the very same dual purpose.

In my mind, `Residual` or `TryAs` live in the "homogeneous try universe", and `Try::Kind` lives in the "heterogeneous try universe".

...And "residual types" like `Result<!, E>` live in the intersection of the two universes. So they carry a lot of weight: 1) They must communicate the _specific_ Try type they are derived from for homogeneous purposes, and 2) they must support heterogeneous interoperability by way of `FromResidual` impls. Because of the former, you are forced to write more impls for the latter.

> [@MusicalNinjaDad](#):
>
> > [@camsteffen](#):
> >
> > The ship is probably sailed for having `From` built into `?`, and for that reason we probably need `FromBreak` anyways.
> 
> No ships have sailed until stabilisation. But personally, I'd consider them to be semantically different enough to warrant separation, without even thinking about potential collisions in blanket implementations.

I have `ControlFlow` in mind specifically when I say the ship has sailed because I think it would be backward-incompatible to add `From` conversion to `ControlFlow`. But maybe we could do that over an edition boundary?

* * *

I'd like to think that this proposal can be decomposed into smaller orthogonal (to a degree) decisions.

- Heterogeneous Try: "residual types" vs. `Kind`
- Homogeneous Try: `Residual` vs. `TryAs`
- `From` in `FromBreak`/`FromResidual` vs. `From` in `?`
- Complex Try Types (combined semantics), options include:
  - `Try::from_break` - no support
  - `FromBreak` without `FromBreak::Kind` - limited to a single `Try::Kind` - basically "no support"
  - `FromResidual` - supported via "residual types"
  - `FromBreak::Kind` - supported via `Kind`

---

<div class="post-metadata">

**Author:** ![camsteffen](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/camsteffen/32/3246_2.png) [@camsteffen](https://internals.rust-lang.org/u/camsteffen)\
**Post date:** [September 28, 2026, 3:22pm UTC](https://internals.rust-lang.org/t/pre-rfc-another-attempt-to-re-design-the-try-trait-try-kind-and-tryas/24618/6 "2026-09-28T15:22:45Z")

</div>

Here's what the traits could look like if we keep `Try::Kind` and minimal "break types" but extend it with support for complex try types. `TryAs` and homogenous try is the same as OP. This can work with or without `From` in `?` desugaring.

```rust
pub const trait Try: TryFromBreak<Self::Break, Self::Kind> {
    type Kind;

    type Continue;

    type Break;

    fn branch(self) -> ControlFlow<Self::Break, Self::Continue>;

    fn from_continue(c: Self::Continue) -> Self;
}

pub const trait TryFromBreak<Break, Kind> {
    fn from_break(b: Break) -> Self;
}

pub const trait TryAs<T>: Try {
    type Try: Try<
	    Kind = Self::Kind,
	    Break = Self::Break,
	    Continue = T,
    >;
}

```

Example impl:

```rust
impl<T, E> ops::Try for Result<T, E> {
    type Kind = Result<!, !>;
    type Continue = T;
    type Break = E;

    fn branch(self) -> ControlFlow<E, T> {
        match self {
            Err(e) => ControlFlow::Break(e),
            Ok(v) => ControlFlow::Continue(v),
        }
    }

    fn from_continue(t: T) -> Self {
        Ok(t)
    }
}

impl<T, E> TryFromBreak<E, Result<!, !>> for Result<T, E> {
    fn from_break(e: E) -> Result<T, E>{
        Err(e)
    }
}

```

**The Heterogeneous `?` Rule** :

```rust
Let `T` = the `?` target.
Let `R` = the try scope result

R: TryFromBreak<<T as Try>::Break, <T as Try>::Kind>

```

* * *

This maintains the advantage of higher re-usability for a custom result type. If you use `Try::Kind = Result<!, !>`, you get two-way interoperability with std `Result` with writing only one `impl TryFromBreak`.

---

<div class="post-metadata">

**Author:** ![camsteffen](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/camsteffen/32/3246_2.png) [@camsteffen](https://internals.rust-lang.org/u/camsteffen)\
**Post date:** [September 28, 2026, 3:39pm UTC](https://internals.rust-lang.org/t/pre-rfc-another-attempt-to-re-design-the-try-trait-try-kind-and-tryas/24618/7 "2026-09-28T15:39:25Z")

</div>

With this version, it may be more preferable to have dedicated types for `Kind` especially for readability in `TryFromBreak`.

```rust
/// The `Try::Kind` for types where `?` tests for the presence of an error.
/// The canonical type with this `Try::Kind` is `Result`.
struct TryResult(());

impl<T, E> Try for Result<T, E> {
    type Kind = TryResult;
    ...
}

impl<T, E> TryFromBreak<E, TryResult> for Result<T, E> {
    ...
}

```
