Pre-RFC: Catching Functions

I agree. I've been wanting to write-up something for a while now, but I've been pretty busy. My feeling is that this thread has brought up four important questions:

  1. Do we want this feature at all? Will it impact learnability?
    • Drilling into the motivation more seems important.
    • My take is that this can be a win for ergonomics and learnability (which I see as distinct, but related, goals)
    • As far as ergonomics goes, I often find the need to add Ok to be kind of a "mental impedance mismatch" -- I forget it, then get an obscure compiler error, then add it. I think this is because code like foo()? + bar()? never has to talk about Err explicitly, so in a way it's odd to have to talk about Ok
    • As far as learnability, that same mismatch means that to learn error handling one has to fully grok its corners. I think it could be very useful to think of error handling as "exceptions that one must manually propagate at each level" and then, once you've learned about enums etc, understand how that works under the hood.
    • But obviously things like Swift can act as a counter example: at some point, particularly when the abstraction is too "leaky", syntactic sugar just means you have to learn both the sugar and the underlying thing, and that's not a net win for learnability.
  2. Given that we want this feature, should the source code expose the "inner type" (e.g., T catches E) or "outer type" (Result<T, E>). This applies equally to async fn
    • I don't know the answer here. I think I'd like to try it both ways.
    • I can see that using the "outer type" might have the same mental impedance problem ("I return a value of type T, but my return type is declared as Result").
    • On the other hand:
      • I can see that makes the transition to full understanding more smooth;
      • It helps when reading the signature of a fn you are about to call
      • It eliminates a potential inconsistency with rustdoc
      • It allows smooth support of Option
      • It has precedent from other TypeScript and C# for async fn at least
    • I lean towards writing the "outer type"
  3. Given that we want the feature, how should we denote that you are using it? This is not independent from the previous question.
    • T caches E (assumes internal type)
    • try fn foo() -> Result<T, E> (assumes external type)
    • catch fn foo() -> Result<T, E> (assumes external type)
    • fn foo() -> Result<T, E> try { ... }
    • fn foo() -> Result<T, E> catch { ... }
    • There are also blocks to consider:
      • catch R { ... }
      • try as R { ... } ?
    • I think consistency with async fn is probably valuable here
      • note that async { ... } blocks are also a thing
  4. Given that we want this feature, is using the trappings of exception handling helpful or harmful?
    • There is after a key difference in the semantics of this feature: errors do not automatically unwind through multiple frames but rather must be manually propagated with ?
    • There are also implementation differences (e.g., we don't use typical unwinding techniques) but those are less imp't to my mind; after all, exceptions can be and sometimes are implemented using hidden return flags
    • The core question is, will using "exception terminology" be helpful to people learning or confusing?
    • I tend to think it is helpful: I think having something familiar makes it easier to learn the ways that error handling is different, in short.
      • This isn't always true. The match between the syntax and the concepts have to be close enough. I think they are in this case.
    • This does imo argue slightly against the keyword catch (as opposed to say try) -- using familiar keywords but in an unconventional way means that now you sort of have two things to learn. I'm not sure that's a net win.

Anyway, that's my best summary of how I see the state of the thread just now. I do hope to find the time to write out thoughts in more detail.

13 Likes