Introducing an “Unwrap or Throw” operator

Hi S/E,

This thread is the love child of two previous contentious threads [Pitch] Introducing the “Unwrap or Die” operator to the standard library and more recently Moving toward deprecating force unwrap from Swift?. The latter thread did not reach a conclusion other than being locked and this thread picks up on the former thread proposing instead an “Unwrap or Throw” operator might be a better solution to the problem of forced unwraps in Swift which bugs me and I'm sure it bugs our users when their apps crash out.

In short, I’d like to propose a !! operator, a cross between a forced unwrap and nil coalescing that throws if an optional is nil which you could use in any of the following ways;

try URL(string: "https://google.com") !! fatalError("WTF?")
try URL(string: "https://google.com") !! NSError(domain: "WTF", code: -2, userInfo: nil)
try URL(string: "https://google.com") !! { throw NSError(domain: "WTF", code: -2, userInfo: nil) }
try URL(string: "https://google.com") !! "WTF?"
try URL(string: "https://google.com") !! showUserHelpfulErrorMessageAndQuit()

I’ve gone through a number of iterations on this problem but this is the first option I’ve found ticks the following boxes:

  • Flexible and convenient to use.
  • Fails fast on debugging - does not gloss over errors making them difficult to debug
  • Throws on a release build instead rather than an unceremonious abort of the app

I’ve resisted turning a force unwrap into a throw in the past as it seems to disrupt the flow of your code but that is exactly what you want it to do. It’s a bit more work but whenever you force unwrap you need to think about the “what if the optional is nil” code path anyway.

The operator is generic and you can provide a String to be logged in the error thrown or something that conforms to Error directly or any swift expression or a block if you want to make the throw explicit. I would pitch to have this in stdlib but if you use it as a Swift Package you have the advantage that it always “fails quickly” and aborts into the debugger if the value is nil whereas in a Release build it uses the throw mechanism to effectively “goto” a recovery code path and the app can battle on or at least present an error dialogoue.

The code for this is reasonably straightforward and is available as my least starred Swift Package

infix operator !!: NilCoalescingPrecedence

extension Optional {
    /// An Error thrown if you don't provide one.
    public enum UnwrapError: Error {
        case forceUnwrapFailed(text: String, file: StaticString = #file, line: UInt = #line)
    }
    /// Preferred "Unwrap or throw" operator.
    /// Always fails quickly during debugging for exceptional
    /// conditions but throws in a "Release" build motivating
    /// the developer to provide an error recovery path if an
    /// Optional is nil and not just crash the application out.
    /// - Parameters:
    ///   - toUnwrap: Optional to unwrap
    ///   - alternative: Message or Error to log/throw on nil
    /// - Throws: UnwrapError showing reasoning
    /// - Returns: unwrapped value if there is one
    public static func !!<T>(toUnwrap: Optional,
                          alternative: @autoclosure () -> T) throws -> Wrapped {
        switch toUnwrap {
        case .none:
            try unwrapFailed(throw: alternative())
        case .some(let value):
            return value
        }
    }
    /// Alternative as a fuction which has the advantage it
    /// documents the file and line number.
    public func unwrap<T>(orThrow alternative: @autoclosure () -> T,
        file: StaticString = #file, line: UInt = #line) throws -> Wrapped {
        switch self {
        case .none:
            try Self.unwrapFailed(throw: alternative(), file: file, line: line)
        case .some(let value):
            return value
        }
    }
    /// Internal function performing the actual throw
    private static func unwrapFailed<T>(throw alternative: T,
        file: StaticString = #file, line: UInt = #line) throws -> Never {
        if let throwingClosure = alternative as? () throws -> Void {
            try throwingClosure()
        }
        let toThrow = alternative as? Error ??
            UnwrapError.forceUnwrapFailed(text: "\(alternative)", file: file, line: line)
        #if DEBUG
        // Fail quickly during development.
        fatalError("\(toThrow)", file: file, line: line)
        #else
        // Log and throw for a release build.
        // Gives the app a chance to recover.
        NSLog("\(toThrow)")
        throw toThrow
        #endif
    }
}
2 Likes

This feels very similar to the guard statement.

guard URL(string: "https://google.com") != nil else {
    fatalError("WTF?")
}
guard URL(string: "https://google.com") != nil else {
    throw NSError(domain: "WTF", code: -2, userInfo: nil)
}
guard URL(string: "https://google.com") != nil else {
    throw UnwrapError.forceUnwrapFailed(text: "WTF?")
}
guard URL(string: "https://google.com") != nil else {
    showHelpfulErrorMessageAndQuit()
}

Also, force unwraps are not a “problem” — to quote @Ben_Cohen at the bottom of the second thread, they’re a legitimately useful part of the language. A force unwrap, when used properly, will not crash your program. If a force unwrap does crash your program, then you’re not using it properly.

3 Likes

Stepping back, why can’t the right hand side of ?? be a throw or a fatalError? I mean they can replace a return statement, couldn’t they be “upgraded” so we can use them as replacements of any instance, in any location?

1 Like

To this day my preference for this feature hasn't changed:

try someOptional ?? throw myError

// just written but not tested
func ?? <T>(lhs: T?, rhs: @autoclosure () throws -> Void) rethrows -> T {
  guard let unwrapped = lhs else {
    try rhs()
  }
  return unwrapped
}

Something similar would work with fatalError but in the linked thread we said that instead of overloading the operator we rather want to wait until Never becomes the bottom type.

11 Likes

We could wait for "Never to become a bottom type" which solves the the problem for fatalError and functions that return Never (i.e crash) but ideally it would be nice to be able to do something as you say, like the following:

try someOptional ?? throw myError

Unfortunately throw is a statement not an expression so this doesn't parse unless you wrap it in a block. As the !! implementation uses an unconstrained generic for the second argument of the operator unlike ??, the block can be detected which is how I reached this solution (which compiles in Xcode 11)

1 Like

Unfortunately, I do not think this pitch is well considered. At first I thought, it may be an interesting method, spelled something like unwrapped(orThrow:)—after all, we do have unsafelyUnwrapped.

Then I realized, we'd actually want to be able to generalize this to allow a default, much like we have a dictionary subscript that allows for specifying a default value if a key isn't found. So, in like manner, we could have unwrapped(default:).

Then I realized that that'd be just ??.

The two differences between what's pitched here and ?? are: (1) It doesn't allow for the right-hand side to supply a default and in fact discards the return value. That is both inconsistent with ?? (and, for that reason, a potential footgun) and unnecessarily restrictive. (2) It uses an autoclosure, which is convenient at the point of use but is inconsistent with ??, with the tradeoff of brevity for clarity at the point of use (there is nothing about the spelling of one that signals that it differs from the other in right-hand side "autoclosure-ness").

I do not think that, even if there were not those tradeoffs and potential footguns, eliminating { ... }() at the point of use passes the bar for adding an API to the standard library, let alone a new operator. ("The bar" being those criteria laid out in the past which I will not recite again here in their entirety, but which includes asking whether the pitched API solves some performance or correctness pitfall in the status quo.) With the tradeoffs and potential footguns...

2 Likes

In evaluating the idea, it's important to separate the proposed !! operator from the semantics of the existing ?? operator as they are quite different beasts by design - even if they appear similar (If anything it's closer to the operation of ||) It's more related to and only intended to be an alternative to a force unwrap. It's better out of the stdlib as using it as a Package has the "failing fast during development" advantages I mentioned above. There is a longhand unwrap(orThrow:) available in the code above but it is not at all pretty to use.

I don't understand. Both ?? and this !! use @autoclosure for their right-hand side.

1 Like

Is there a technical reason for why the statement cannot be captured by @autoclosure?

In fact I think that in theory the following signature rhs: @autoclosure () throws -> Never should be sufficient to solve the problem. The compiler only needs to learn how to wrap throw error.

I added Never because we want to signal that the captured closure always throws but never returns.

Then this should do the whole trick:

func ?? <T>(lhs: T?, rhs: @autoclosure () throws -> Never) rethrows -> T {
  guard let unwrapped = lhs else {
    try rhs()
  }
  return unwrapped
}

This is a distinction the compiler makes internally at a very low level: declarations, statements (flow of control, throw return etc.) and expressions (function calls which include assignments as they are all just overloaded operators). Operators can currently only operate on expressions.

I might misunderstand here something. I still don't see why @autoclosure should not be able to capture whole statements.

// everyone knows that @autoclosure will wrap everything into a block / closure
throw myError ---> { throw myError } 

We already can use the closure syntax without @autoclosure and get the expected result. All we want to achieve here is to make it a bit more convenient and implicit.

And if we're at it, I think it might be a great idea to introduce a tiny breaking change to the language for Swift 6:

let foo = {
  throw MyError.bar
}

// currently: `foo: () throws -> Void`
// after: `foo: () throws -> Never` 
// If the compiler knows that every exit path of a function throws
// the function itself should never return, hence `Never`

let bar: () throws -> Void = {
  if someCondition {
    print("swift")
  } else {
    throw MyError.bar
  }
}

I move the last idea into its own pitch. ;)

1 Like

Good point: a think-o on my part. That one autoclosure works with throws and the other doesn’t is the inconsistency then—and I see folks are exploring why that is.

I want part of this functionality, but...

  • It shouldn't be an operator.
  • It doesn't need to handle every case proposed, given how rare many of them are. In particular, I don't think it needs to handle fatal errors or custom messages by default.

So, as @xwu mentioned, I think this functionality should simply be unwrapped(orThrow:) that produces a new UnwrapError or a provided Error instance. I add this to all of my projects anyway, as it's a good way to bridge optional and throwing code without having to manually unwrap everything.

1 Like

I have been using this solution for ages and haven't felt a need for a custom operator or a method on Optional:

A standard UnexpectedNil error might be nice though.

7 Likes

Thanks @mayoff! That is a very, very useful link. It's all there from 2018 including discussions about throw as an expression etc. I've pushed a new version of the Unwrap Swift Package with my take on @Joe_Groff's suggestion (with all the elegance and simplicity taken out:)...

/// Function that allows you to throw but in an expression.
/// Intended for use after nil coalescing operator in unwrap.
/// Gives fatalError in Debug builds to aid debugging and
/// throws in a Release build for app to be able to recover.
/// - Parameters:
///   - toThrow: A String, Error or closure that throws
/// - Throws: Error passed in or UnwrapError
/// - Returns: Never
public func unwrapFailure<E,T>(throw toThrow: E,
    file: StaticString = #file, line: UInt = #line) throws -> T {
    if let throwingClosure = toThrow as? () throws -> Void {
        try throwingClosure()
    }
    let toThrow = toThrow as? Error ?? Optional<T>.UnwrapError
        .forceUnwrapFailed(text: "\(toThrow)", type: Optional<T>.self,
            file: file, line: line)
    #if DEBUG
    // Fail quickly during development.
    fatalError("\(toThrow)", file: file, line: line)
    #else
    // Log and throw for a release build.
    // Gives the app a chance to recover.
    NSLog("\(toThrow)")
    throw toThrow
    #endif
}

Something like this probably should be in the standard library and more widely known even if it is a global function. You can indeed now just type the following which seems a pretty good solution:

try someOptional ?? unwrapFailure(throw: myError)
1 Like

I sometimes use this in my code bases:

@_disfavoredOverload
public func ??<Wrapped>(optional: Wrapped?, error: @autoclosure () -> Error) throws -> Wrapped {
    guard let wrapped = optional else { throw error() }
    return wrapped
}
3 Likes

I agree that the unwrap failure is ergonomic. There should be better debugging tooling for nil unwrapping. I once had a crash in an App Store submission for AR MultiPendulum, and I had to go through a lengthy crash log without any description of the error. I think it was from a failed nil unwrapping.

In my code, I tend to use something like this, to both give better crash logs, as well as self-document at call site:

extension Optional {
  func safelyUnwrapped(because reason: String) -> Wrapped {
    guard let unwrapped = self else {
      fatalError("Failed non-nil assertion: \(reason)")
    }
    return unwrapped
  }
}

Then at call site, e.g:

let url = URL("http://swift.org").safelyUnwrapped(because: "url is known at compile-time to be valid")
1 Like

Am I correct in thinking that there are multiple different desires in play?

  1. "unwrap or throw" as just sugar to transform an optional unwrap to a throwing error.
  2. "unwrap or die" alternatives to force unwrapping (and crashing if nil) because we want to be explicit about the reasons.

I see these things as separate goals. The linked package seems to focus on number 2 because it fatalError on DEBUG.

I personally always want 1, never 2. I want sugar to throw an error if nil, but that's something I expect to happen, is not an edge case that needs to fatal error.

IMO ideally we should have both behaviours using the same syntax and being explicit.

try optional ?? throw(Error())
optional ?? fatalError("fancy message")
1 Like

Ideally, Never would be a proper bottom type and the second syntax would just work out of the box. As for the first syntax, there's nothing wrong with it, but I'm not entirely sure if it's a common enough need to be in the standard library.

While I generally dislike NPM style micro-libraries, it might be best to put both versions of ?? into a library and see how often people use it. The implementation is actually quite simple:

@inlinable
public func ?? <T>(value: T?, failure: @autoclosure () -> Never) -> T {
    guard let value = value else { failure() }
    return value
}

@inlinable
public func ?? <T, E>(value: T?, error: @autoclosure () -> E) throws -> T where E: Error {
    guard let value = value else { throw error() }
    return value
}

EDIT: Actually, it seems that the Never version does work out of the box now, but you still need to include the definition for it if you want to use both that and the Error version, because otherwise it'll try and call the throwing version when you put fatalError on the RHS.

1 Like