How to catch typed throws in clousures

demo:

struct AError: Error {}

func a() throws(AError) {}

func b() {
  do {
    try a()
  } catch {
    _ = error as AError // ok
  }

  let block = {
    do {
      try a()
    } catch {
      _ = error as AError // error: 'any Error' is not convertible to 'AError'
    }
  }
}
struct AError: Error {}

func a() throws(AError) {}

func b() {
  do throws(AError) {
    try a()
  } catch {
    _ = error
  }

  let block = {
    do throws(AError) {
      try a()
    } catch {
      _ = error
    }
  }
}
3 Likes

Interesting. Why is do throw notation required in case of that "do" statement being inside of a closure whilst it is optional when the do statement is inside of a function?

3 Likes

Thanks it works!

However, I met the second problem… Combine unwrap and typed throws…

struct AError: Error {}

let a: Int?

func b() throws(AError) -> Int { 1 }

do throws(AError) {
  // I want to make it inline
  let c = try a ?? b() // error: thrown expression type 'any Error' cannot be converted to error type 'AError'
} catch {}
1 Like

This one is a different bug as it happens without a block.

Test
struct AError: Error {}

var a: Int? = 0

@MainActor func b() throws(AError) -> Int { 1 }

@MainActor func foo() {
    do throws {
        let c = try a ?? b()
    } catch {}
    
    do throws(AError) {
        let c = try a ?? b() // Error
    } catch {}
}

I think the cause to the error is 2-layered:

  1. ?? is defined as a function that does not recognize typed throw (yet): func ?? <T>(T?, @autoclosure () throws -> T) rethrows -> T // FIXME: typed throws
  2. arguments passed to @autoclosure parameters needs to be transformed into the form of typed-throws closures, but now they are treated just like normal throwing closures.

The best way I can find to express your idea is using if-expressions:

do throws(AError) {
  let c = if let a { a } else { try b() }
} catch {}
5 Likes

Thanks! Although ugly but it works!

As Mr. Wu pointed out, ?? operator doesn’t currently support typed throws. Depending on your use case, Mr. Wu’s answer is decent, but I’d like to suggest an alternative (a very rough idea):

extension Optional {
func unwrap(
  defaultValue: Wrapped
) -> Wrapped {
    switch self {
    case .some(let v): v
    case .none: defaultValue
    }
  }
}

struct AError: Error {}


let a: Int? = nil

func b() throws(AError) -> Int { 1 }

do throws(AError) {
  let c = try a.unwrap(defaultValue: b())
} catch {}

// Or

do throws(AError) {
  let c = try { () throws(AError) -> Int in
    switch a {
    case .some(let a): a
    case .none: try b()
    }
  }()
} catch {}

Typed throws are underdeveloped. They lose type information via autoclosures. So you can safely overload ?? to take a not-auto-closure:

extension Optional {
  static func ?? <Error>(
    optional: consuming Self,
    defaultValue: () throws(Error) -> Wrapped
  ) throws(Error) -> Wrapped {
    if let wrapped = consume optional { wrapped }
    else { try defaultValue() }
  }
}

This is better, when you have a function defined. It is hideous when you do not, however:

do {
  let c = try a ?? b
  let d = try a ?? { () throws(AError) in try b() }
} catch is AError { } // 'is' test is always true

Also, the title of this thread should be changed. "Block" is an Objective-C term. "Closure" is the Swift nomenclature.

1 Like

This is almost good but loses the autoclosing. To get around the bug, you can put the error type into a tuple. Painfully.

extension Optional {
  static func ?? <Error>(
    optional: consuming Self,
    defaultValue: @autoclosure () throws(Error) -> (Wrapped, Error.Type)
  ) throws(Error) -> Wrapped {
    if let wrapped = consume optional { wrapped }
    else { try defaultValue().0 }
  }
}
do {
  let c = try a ?? (b(), AError.self)
} catch is AError { } // 'is' test is always true

As shown in my post above, that's not exactly the bug. The type information is not lost; it just cannot be used implicitly.

// Abbreviation for "exhibit autoclosure bug".
func eab<Error>(
  _: @autoclosure () throws(Error) -> some Any,
  _: Error.Type = Error.self
) throws(Error) { }


enum 👺: Error { }
func throw👺() throws(👺) {
  eab("✅", Never.self)
  eab("🐞") // Generic parameter 'Error' could not be inferred
  
  try eab("✅", 👺.self)
  try eab(throw👺()) // Generic parameter 'Error' could not be inferred

  try eab("❌", (any Error).self) // Thrown expression type 'any Error' cannot be converted to error type '👺'
}

I think it's a bug. Simplifying your code,

This compiles:

enum MyError: Error {}

func typedThrow() throws(MyError) {}

func typedThrowWithNormalClosure(_ : () throws(MyError) -> Void) throws(MyError) {}

func typedThrowWithAutoclosure(
    _ : @autoclosure () throws(MyError) -> Void
) throws(MyError) {}

func typedTest() throws(MyError) {

    try typedThrowWithNormalClosure({ () throws(MyError) -> Void in
        try typedThrow()
    }) // ✅

    try typedThrowWithAutoclosure(typedThrow()) // ✅
}

The generic version + autoclosure doesn't compile:

func genericTypedThrow<T: Error>(_ v: T.Type) throws(T) {}

func genericTypedThrowWithNormalClosure<T: Error>(
    _ : () throws(T) -> Void
) throws(T) {}

func genericTypedThrowWithAutoclosure<T: Error>(
    _ : @autoclosure () throws(T) -> Void
) throws(T) {}

func genericTypedTest<T: Error>(_ v: T.Type) throws(T) {
    try genericTypedThrowWithNormalClosure({ () throws(T) -> Void in
        try genericTypedThrow(v)
    }) // ✅

    try genericTypedThrowWithAutoclosure(genericTypedThrow(v)) // ❌
}
3 Likes

Good find. I don't think this will ever get fixed, so it may be more practical to have Claude Code generate all the overloads your project needs.

E.g. in project:

public struct AError: Error {}

public extension Optional {
  static func ?? (
    optional: consuming Self,
    defaultValue: @autoclosure () throws(AError) -> Wrapped
  ) throws(AError) -> Wrapped {
    try optional ?? (defaultValue(), AError.self)
  }
}

func test() throws(AError) -> Int {
  let a: Int? = nil
  func b() throws(AError) -> Int { 1 }
  return try a ?? b()
}

And in shared package for all projects:

public extension Optional {
  static func ?? <Error>(
    optional: consuming Self,
    defaultValue: @autoclosure () throws(Error) -> (Wrapped, Error.Type)
  ) throws(Error) -> Wrapped {
    if let wrapped = consume optional { wrapped }
    else { try defaultValue().0 }
  }
}

Errr, yes you could... technically... as short circuiting is not inherently bound to the operator ?? itself (just its concrete func implementation). As for the operator – it's merely a convention, similar to that used for other operators like ||. However, if to follow the spirit of that convention I'd use a different symbol that doesn't suggest short-circuiting behaviour... for example optional | defaultValue() if to reuse / hijack | from bitwise ops.


Having said that, in real code I'd probably take the path of least resistance to workaround this limitation and use the code suggested by @CrystDragon :

// TODO: check this in a year or three to see if typed throws work better with autoclosures
// test1: let c = try a ?? b()
// test2: let c = a ?? (try b())
// until then will be using this workaround:
let c = if let a { a } else { try b() }