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
}
}