Remove redundancy in Rethrowing Functions when `try?` is used

My proposal is to remove the need for a concrete error to be used in rethrowing functions when the optional try? variant is used to call it.

This is a very small suggestion - I came across it as a small inconvenience while developing yesterday. I was using reduce(into:_:) to construct a dictionary, like so:

       func parseData(array: [MyObject]) -> [[String: Any]]? {
            try? array.reduce(into: [[String: Any]]()) {
                guard let dict = $1.dictValue() else {
                    throw ReportError.parsingFailed
                }
                $0.append(dict)
            }
        }

MyObject.dictValue() here is a throwing function. Since I only wanted a final value, and only wanted to return nil when the parsing failed, I could efficiently escape the reduce function and return nil immediately.

Since I was using try? and returning nil on failure, I did not care what error was thrown and I did not need to handle it. The compiler forced me to add a concrete error case here.

The updated code would look much the same, except with the concrete error removed:

    private func parseData(array: [MyObject]) -> [[String: Any]]? {
        try? array.reduce(into: [[String: Any]]()) {
            guard let dict = $1.dictValue() else { throw }
            $0.append(dict)
        }
    }

I feel this is much neater and helps to make the language a tiny bit easier to reason about when using rethrowing functions.

This is my first post on the Swift Evolution forums and so my apologies if I pitched this incorrectly - please let me know if there are ways I can improve on it for next time.

Additionally, I can imagine that this could be very complex to implement since it essentially requires the compiler to reason about two separate functions. If you think it is far too complex (for a marginal benefit), I'd be interested to hear more about why this is.

The compiler would have to prove that no caller inspects the error. While this is feasible for non-public methods, it wouldn't be possible to make that assumption for cross-module callers.

I have often needed something similar. What I usually do is something like this:

func throwsButSpecificErrorUnimportant() throws {
    struct E: Error {}

    ...

    throw E()
}
1 Like

I wonder if there is – or should be – some “default” or “standard” error which can be thrown in such a situation, or if the error is catched locally:

do {
    // do something ...
    if exceptionalCase throw Error.default
    // continue doing something ...
} catch {
    // handle exceptional case
}
1 Like

You can throw NSError().

You have to make up some domain and code, otherwise you'll get a

let err = NSError()
// -[NSError init] called; this results in an invalid NSError instance.
// It will raise an exception in a future release. Please call
// errorWithDomain:code:userInfo: or initWithDomain:code:userInfo:.

(Tested with Xcode 11.3.1 in a command line application.)

1 Like

I tried in a playground and received no warnings.

NSError is a good workaround, but what if there was a way to do this that didn't rely on Cocoa APIs?

Martin's original suggestion can't be implemented as a static member on the Error protocol, but could be done as a global function. The fileprivate modifier and an opaque return type would remove the issue of having a new free-floating Standard Library type:

    fileprivate enum _AnonymousError: Error {
        case `default`
    }

    public func anonymousError() -> some Error {
        _AnonymousError.default
    }

I tested it in Xcode and this can be called from outside due to the opaque return type of Error.

So, the function can throw both from your lookup method and from explicitly throwing after testing for nil, right? It seems that the second way is there only because you want to force errors in the second way to share handling from the first way, right? I think the best solution is not to force one way of error handling into another and use normal processing for each style.

Here, that means dumping reduce for a conventional for-in loop. Guard on each extraction call to return nil upon failure. Surround the loop with a do-catch to return nil on throw.

Just mentioning that an earlier implementation of Sequence.first(where:) used an internal error for flow control:

@usableFromInline
@_frozen
internal enum _StopIteration : Error {
  case stop
}

extension Sequence {
    // ...
    @inlinable
    public func first(
      where predicate: (Element) throws -> Bool
    ) rethrows -> Element? {
      var foundElement: Element?
      do {
        try self.forEach {
          if try predicate($0) {
            foundElement = $0
            throw _StopIteration.stop
          }
        }
      } catch is _StopIteration { }
      return foundElement
    }
    // ...
}

so apparently there can be reasons to do so. (This was reported as SR-3166 and changed later.)

Might be interesting to be able to throw nil as a default, maybe?

1 Like

Having throw fall back to a default error seems like a bad idea for general code, and adding a whole complex analysis to allow it in narrow cases seems awfully specialized.

7 Likes