[Pitch N+1] Typed Throws

Thank you @Alex_Martini, that's really helpful!

The discussion on the patch is particularly insightful. Excerpt:

@jrose: If we ever add typed errors to the language, then we'd want to make sure the only thing thrown is the error that was caught. Even now it might be a nice restriction that's still in the spirit of rethrows : the function will never throw its own errors.

@Lily_Ballard: I actually disagree about the typed errors; the catch block would have to only throw errors that are part of the declared set of types, but I see no reason why it can't return a different error, such as one that wraps the error thrown by the parameter.

Furthermore, I don't think we can reasonably define "the function will never throw its own errors" without being needlessly restrictive. For example, my own code that motivated this change looks like

func scoped<T>(key: String, @noescape f: () throws -> T) rethrows -> T {
   do {
       return try f()
   } catch let error as JsonError {
       throw error.withPrefix(key)
   }
}

(where JsonError.withPrefix() returns a new error that has the given key prefixed onto the access path)

This function is not throwing its own error, it's merely modifying the error that was thrown, but I don't see how to define "throw its own errors" in any meaningful fashion that doesn't prevent this usage.

@jrose: Well, it depends on whether "rethrows" means "only throws if the closure throws" or "only throws what the closure throws". That seems like an important distinction that may be worth discussion.

So yeah, all things old new again. :laughing:

It goes on - and I strongly encourage everyone here to go read the full thread - but there's one more bit that I want to highlight:

@jrose: I don't know. If the rethrows code is in a library, it seems weird to get an error out that I didn't throw. If I'm deliberately throwing an error that contains information about my crash, I'd be very upset if I couldn't catch it. Any client of a rethrowing function that actually wants to catch the error now needs to know how to get it out of the one that's actually thrown.

I didn't see any of us talk about this yet in this current thread, but it's a really good point that @jrose made way back then.

Typed throws can help a little if we let them - e.g. if we allow rethrows(X) or rethrows throws(X) then at least the caller knows what they're actually getting, and can hopefully accomplish what they want with that.

Sum types (or equivalent) would be a bit better again. Combined with typed errors, that lets you explicitly express whatever conceivable type behaviour you actually want, as a 'rethrowing' function - e.g. any of:

  • (type-wise) I throw only whatever my closure(s) throw, or
  • I throw only this other error type (a wrapper maybe, with extra info I add), or
  • I throw either whatever my closure(s) throw or something else.

As far as I can see there's perfectly valid (and used) use-cases for each of these, which is no surprise given how fundamental errors & their handling is to any programming language.

I don't see a good way with just these mechanisms to express "I throw exactly the error instance my closure(s) throw", or the "pure rethrows" case. I'm intrigued by @dmt's suggestion of named errors.

Or maybe a lateral solution will prove best?

e.g. what if the Error protocol added some mechanism to add arbitrary key-values to an error, so you don't have to wrap errors and change their types for some common operations?

Or, what if there were a way to dynamically subclass (essentially if not literally) the error type of the closure so that you can add your own properties and/or overrides without breaking callers that catch based on the [now parent class] type? Kinda implies errors become either actual classes (seems like a horrible idea - enums as errors is awesome) or some special new metatype which is a hybrid of enums and classes, or somesuch. A very left-field idea, sure - just brainstorming.

1 Like