Throw on nil

I find myself wanting to define an operation that converts failable initializers to throwing ones. When I have a throwing initializer contains multiple failing initializers, I think it improves the clarity and readability of the code. (see below)

UPDATE: Leaving the original implementation but I am not opposed to changing the spelling or making this an operator or whatever. Lots of great ideas and info below.

extension Optional {
  func orThrow(_ error: @autoclosure () -> Error) throws -> Wrapped {
    if let wrapped = self {
      return wrapped
    } else {
      throw error()
    }
  }
}
6 Likes

This is most recent, I believe:

2 Likes

This was added recently-ish to XCTest (as XCTUnwrap); it's very useful there, and it seems reasonable to have it available in non-test code too.

8 Likes

This is one of those additions that's so easy to add and just syntax sugar so I imagine gets deferred indefinitely.

However, I think not having it actively encourages less robust error handling. This comes in the form of either using bogus nil coalesced default values or using compactMap where it shouldn't be. It's a very small change that encourages better error handling.

Also, it makes a nice analog to try?.

6 Likes

:+1: I use it and would love to not have to maintain my own version.

Two changes I'd like to see:

  1. A default argument.
  2. A different name. Here is mine, but I'll let everyone else bikeshed it, and then I'll use whatever you think is best if it can get added. :biking_man:
public extension Optional {
  struct UnwrapError: Error {
    public init() { }
  }

  /// - Note: Useful for emulating `break`, with `map`, `forEach`, etc.
  /// - Throws: if `nil`.
  func unwrap(
    orThrow error: @autoclosure () -> Error = UnwrapError()
  ) throws -> Wrapped {
3 Likes

Yes, I've used a form of this in every project I've been on in the last 4 years. It's extremely useful in throwing contexts. I think the biggest issue (aside from a lack of interest in the core team) would be naming and the exact signature. Swift doesn't have a precedent for or* methods. The closest existing API would be get() on Result, but that method name is horrible.

4 Likes

I'll note as the person who originally pushed for try? that I regret adding it. The most common use is to ignore errors, and that should have been a free function ignoringErrors(_:) (like withoutActuallyEscaping(_:do:)). Throwing away an error without logging it or anything is poor practice and should be made more explicit when it is the right thing to do.

(But I do think that either ?? throw or a method on Optional makes sense. That's adding information, not dropping it.)

8 Likes

In my private extensions to the standard library, I have the "unwrap or die" operator (!!), but I also have the "unwrap or throw" operator too:

let maybeValue: Optional<Foo> = ...

let crashIfNil = maybeValue !! "Logic dictates this should never be nil"

let throwIfNil = try maybeValue ?! MyError.missingValue

I've found both to be incredibly useful and I'm sad the pitch wasn't accepted.


This is the implementation:

infix operator !!: NilCoalescingPrecedence
infix operator ?!: NilCoalescingPrecedence

public func !!<T>(value: T?, error: @autoclosure () -> String) -> T {
    if let value = value { return value }
    fatalError(error())
}

public func ?!<T>(value: T?, error: @autoclosure () -> Error) throws -> T {
    if let value = value { return value }
    throw error()
}
7 Likes

Well, even if you log something, who cares if no one is reading those logs in production? Not every developer can afford remote error reporting (*).

Last time I saw try?, it was in order to gracefully ignore some Metal/SceneKit errors, and just disable some unavailable rendering features. Whenever this information gets any value, the error won't be thrown away. Until then, try? was just the perfect tool, when placed at the correct location. Got an error creating the ambiant occlusion renderer? OK, let's just ignore ambiant occlusion.

(*) Whenever Apple is able to report crashes correctly, they may eventually report regular errors as well? This is a very valuable feature of Crashlytics.

5 Likes

I like it. Maybe the error could have some type information about the Wrapped type. It would be nice not have to define it every time I opened a new playground. :slight_smile:

1 Like

Thanks for the info. Was the main objection that they are operators? I have used a variant of !! in some of my projects before.

The decision is outlined here

2 Likes

I agree with the op this is a long-standing "issue" that I always look at and think, "Uhm, wasn't this solved yet?".

I'm a fan of optional ?? throw Error and I think it "makes sense" from a language feel perspective. This is sort of what Kotlin lets you do as well, so there is already some precedence in other modern languages. The trick here would be to understand how to fix the type constraints or specialize that "Never" case (e.g. think optional ?? fatalError())

There is an opportunity to make this a wider proposal to allow sort of a "bottom type" on the right-side of nil-coalescing, and I believe it would be extremely readable and fit the spirit of Swift.

I'm not a huge an of using operators for these sort of things (or most things, actually) given how opinionated those are. They fit well in some team's code base but not all teams' codebases IMO.

I would prefer not having an orThrow(_:) (or same spirit but with different name) but it is a solid option if allowing a more robust solution is out of the question for the Core Team.

3 Likes

Today one can write this:

let result = try someOptional ?? { throw MyError() }()

Or if you prefer something like this:

let result = try someOptional ?? throwError(MyError())

That can be achieved by writing a one-line function:

func throwError<T>(_ e: Error) throws -> T { throw e }

Personally, my preferred solution would be to make throw behave as if it were a bottom type. Then you could simply write:

let result = try someOptional ?? throw MyError()

The same treatment for Never would allow the use of fatalError and any other non-returning functions in arbitrary locations that expect a typed value.

21 Likes

It seems as if others think they always have an error ready to go. I can't get onboard with that idea. This shouldn't go through if we don't have a default way to convert nil to an error.

XCTest uses UnwrappingOptional for XCTUnwrap. Maybe we should deprecate that, and XCTUnwrap along with it, once there's a type available everywhere.

It either has to be a method, or two operators. (Operators with defaults compile, but you can't actually use them. :slightly_frowning_face:)

infix operator ¿¿
postfix operator ¿¿

public func ¿¿ <Wrapped>(
  wrapped: Wrapped?,
  error: @autoclosure () -> Error
    = Wrapped?.UnwrapError() // This can't be used.
) throws -> Wrapped {
  if let wrapped = wrapped {
    return wrapped
  } else {
    throw error()
  }
}

public postfix func ¿¿ <Wrapped>(wrapped: Wrapped?) throws -> Wrapped {
  try wrapped ¿¿ Wrapped?.UnwrapError()
}

I'd love to be able to define the error, nested in these bodies, but the compiler won't allow it yet. So a type either needs to be added to go along with this, or chosen. I don't think there's a good existing candidate for the latter, hence why they made one up for XCTUnwrap?

I like the general principle that operators should be backed by methods. Methods are more flexible but more verbose, while operators are less clear but less verbose. Having both gives you maximum flexibility and choice.

And I agree, there should be a default OptionalError or something that will be the default Error.

1 Like

Why wouldn't we nest it inside Optional?

Something like:

extension Optional {
    struct Missing : Error, CustomStringConvertible {
        let message: String?

        var description: String {
            let base = "Missing expected value of type \(Wrapped.self)" 
            if let message = self.message {
                return "\(base): '\(message)'"
            } else {
                return base
            }
        }
    }
}

func unwrap<T>(_ value: T?, _ message: String? = nil) throws -> T {
    guard let value = value else {
        throw Optional<T>.Missing(message: message)
    }
    return value
}
1 Like

Nesting the Missing error inside Optional makes the Missing type generic. If you want to catch the unexpected-nil error for some reason, you have to know the Wrapped type of the Optional that threw it:

do { ... }
catch let error as Optional<String>.Missing {
    // won't catch Optional<Int>.Missing
}

I'm not sure when it will be useful to catch all unexpected-nil errors, or to catch an unexpected-nil error for any specific Wrapped type. But I'd rather it be possible to catch them all (:musical_note:I wanna be the very best, like no one ever was…), or to catch them just for specific types.

We can make that possible by passing along the Wrapped type in a property instead of as a generic argument:

struct UnexpectedNil: Error {
    var expectedType: Any.Type
}

extension Optional {
    func orThrow() throws -> Wrapped {
        if let wrapped = self { return wrapped }
        throw UnexpectedNil(expectedType: Wrapped.self)
    }
}

Then we can catch all such errors regardless of Wrapped type, or we can use a where clause to catch just a specific Wrapped type:

do { ... }
catch let error as UnexpectedNil where error.expectedType == String.self { ... }
catch let error as UnexpectedNil { ... }
1 Like

As I showed above, that's what I do!

I like what you're going for, but that first line is worse that the generic alternative.

Solution ideas:

  1. Create an AnyOptional type, and nest a protocol inside of it. As the second part of that is currently impossible, we can't put it where it should go:
public protocol AnyOptionalUnwrapError: Error {
public extension Optional {
  struct UnwrapError: AnyOptional_UnwrapError {
  1. Use class inheritance, like key paths do.
public enum AnyOptional { // Or make it a struct, if that's useful.
  public class UnwrapError: Error {
    public init() {
public extension Optional {
  final class UnwrapError: AnyOptional.UnwrapError {
Examples of stuff I use that looks like that, erasing a container of some other sort, including a use case for this pitch:
public enum AnyCaseIterable<Case> {
  public enum AllCasesError: Error {
    /// No `AllCases.Index` corresponds to this case.
    case noIndex(Case)
  }
}

public extension CaseIterable where Self: Equatable {
  /// The first match for this case in `allCases`.
  /// - Throws: `AnyCaseIterable<Self>.AllCasesError.noIndex`
  func caseIndex() throws -> AllCases.Index {
    try Self.allCases.firstIndex(of: self).unwrap(
      orThrow: AnyCaseIterable.AllCasesError.noIndex(self)
    )
  }
}
public extension AnyCollection {
  /// Thrown when `element(at:)` is called with an invalid index.
  struct IndexingError: Error { }
}

public extension Collection {
  /// - Returns: same as subscript, if index is in bounds
  /// - Throws: `AnyCollection<Element>.IndexingError`
  func element(at index: Index) throws -> Element {
    guard indices.contains(index)
    else { throw AnyCollection<Element>.IndexingError() }

    return self[index]
  }
}
1 Like

How about the following for a default Error? I've included the original that accepts an Error to throw as well. Both seem useful to me.

extension Optional {
    struct MissingValueError: Error, CustomStringConvertible {
        let file: StaticString
        let line: UInt

        var description: String {
            "Expected a value of type \(Wrapped.self) at \(file)::\(line)"
        }
    }

    func orThrow(_ error: @autoclosure () -> Error) throws -> Wrapped {
        if let wrapped = self {
            return wrapped
        } else {
            throw error()
        }
    }

    func orThrow(file: StaticString = #file, line: UInt = #line) throws -> Wrapped {
        if let wrapped = self {
            return wrapped
        } else {
            throw MissingValueError(file: file, line: line)
        }
    }
}

let x: String? = nil
try x.orThrow()

Outputs this in my playground:

Playground execution terminated: An error was thrown and was not caught:
▿ Expected a value of type String at MyPlayground.playground::31
- file : "MyPlayground.playground"
- line : 31

Terms of Service

Privacy Policy

Cookie Policy