recently, i’ve been giving typed throws a second look, and evaluating places where i might use it in API. so far, i’ve been disappointed.
at a minimum, i would expect to be able to vend a two-or-more layer API that uses typed throws, where one layer calls the other. for example:
func foo<T, E>(
into initial: consuming T,
with yield: (inout T, Int) throws(E) -> ()
) throws(E) -> T {
try bar(into: initial) {
// ~~~
// Thrown expression type 'any Error' cannot be converted to error type 'E'
try yield(&$0, 1)
}
}
func bar<T, E>(
into result: consuming T,
with yield: (inout T) throws(E) -> ()
) throws(E) -> T {
try yield(&result)
return result
}
but even this minimal example breaks down, you get Thrown expression type 'any Error' cannot be converted to error type 'E'
yes, i know how to fix it — you just need to abandon anonymous closure arguments and write out the full function signature in the closure like this:
- try bar(into: initial) {
- try yield(&$0, 1)
+ try bar(into: initial) { (result: inout T) throws(E) -> () in
+ try yield(&result, 1)
}
but that’s not relevant, i shouldn’t have to know to press this brick in the wall in the first place, i shouldn’t have to explain to my teammates (who are experienced Swift developers!) how to write code that calls this API that compiles, and the fact that i can’t use the abbreviated closure syntax with typed throws anyway is a considerable usability penalty.
skill acquisition for the nuances of typed throws is also problematic. i tried inputting this problem to an LLM, Google Gemini 3.0 Pro, and well, Google Gemini winds up hallucinating a bit, i guess due to how sparsely documented the feature is. the Recommended Solution it gives (add a where E: Error constraint) is completely wrong.
LLM transcript
This is a known limitation in Swift's current type inference system regarding Typed Throws (introduced in Swift 6).
The error happens because the Swift compiler struggles to infer that the closure passed to
barshould specifically throw typeE. Instead, it defaults to the standardthrows(which is equivalent tothrows(any Error)), or it degrades the capturedyieldfunction to throwingany Error.Since your function
fooandbarrequire specific typed throws (E), the compiler flags a mismatch when it thinks the expression inside the closure is throwing a genericany Error.The Fixes
You have two main ways to solve this.
Option 1: Explicitly Constrain
EtoError(Recommended)While Swift 6 allows
throws(E)to implyE: Error, the compiler's inference engine sometimes loses track of this conformance inside closures. Explicitly adding the: Errorconstraint often helps the compiler resolve the types correctly.Swift
// Change <T, E> to <T, E: Error> func foo<T, E: Error>( into initial: consuming T, with yield: (inout T, Int) throws(E) -> () ) throws(E) -> T { try bar(into: initial) { try yield(&$0, 1) } }Option 2: Explicitly Annotate the Closure
If Option 1 doesn't work (or if you can't change the generics), you must explicitly tell the compiler that the closure throws
E.Swift
func foo<T, E>( into initial: consuming T, with yield: (inout T, Int) throws(E) -> () ) throws(E) -> T { // Explicitly define the closure signature try bar(into: initial) { (val: inout T) throws(E) in try yield(&val, 1) } }
Why is this happening?
- Inference Gap: Swift defaults unannotated closures to
throws(any Error)unless the context is extremely strict.- Context Mismatch:
barexpects a closure thatthrows(E).- Conflict: The compiler sees
try yield(...). Due to inference limitations in nested scopes, it perceives this call as throwingany Error(the fallback). It then complains thatany Erroris not specific enough to satisfy the requirement ofE.
ultimately, unless the guarantees provided by static typing of the thrown errors were extremely motivating (rare), the usability penalty inherent in a typed throws API is so severe i would probably assess it to not be worth it at all in most situations.
there have been a few other recent threads started by others about Swift features with noticeable usability handicaps (1, 2), but right now i just want to talk about typed throws and ask why it is so challenging to plug one typed throws API into another?