Paul Calnan's Blog
Published April 16, 2023

On the last two projects I worked on, I have used this extension on Optional:

import Foundation

extension Optional {
    func unwrap(orThrow error: @autoclosure () -> Error) throws -> Wrapped {
        guard let value = self else {
            throw error()
        }
        return value
    }

    func unwrap(or fallback: Wrapped) -> Wrapped {
        guard let value = self else {
            return fallback
        }
        return value
    }
}

The unwrap(or:) function is equivalent to, and more verbose than, the ?? operator. However, I find it works well in a chain of function calls. For example:

func fetchRecord(id: String) async throws -> Record {
    Record(fragment:
        try await client.fetch(query: RecordQuery(id: id))
            .record
            .unwrap(orThrow: ServiceError.notFound)
            .fragments
            .recordFragment
    )
}

func fetchRecords() async throws -> [Record] {
    try await client.fetch(query: RecordsQuery())
        .records
        .unwrap(or: [])
        .map {
            $0.fragments.recordFragment
        }
        .map {
            Record(fragment: $0)
        }
}

This lets me succinctly express a series of operations and transformations in a style that is similar to what you would write using Combine.

Usually, I would stop there. But I was wondering if I could merge the two functions. Something like this:

extension Optional {
    func unwrap(or fallback: @autoclosure () throws -> Wrapped) throws -> Wrapped {
        guard let value = self else {
            return try fallback()
        }
        return value
    }
}

That allows us to write this, just as before:

let value: String? = nil
print(value.unwrap(or: "nothing")

I wanted to be able also to write this, but it doesn't compile:

try print(value.unwrap(or: throw ServiceError.notFound))
error: expected expression in list of expressions
try print(value.unwrap(or: throw SomeError()))
                           ^

Why doesn't that work?

I'm trying to pass throw SomeError() as an autoclosure. This is what the Swift Programming Language book has to say about autoclosures:

An autoclosure is a closure that’s automatically created to wrap an expression that’s being passed as an argument to a function. It doesn’t take any arguments, and when it’s called, it returns the value of the expression that’s wrapped inside of it. This syntactic convenience lets you omit braces around a function’s parameter by writing a normal expression instead of an explicit closure.

So why can't we pass throw SomeError() to as an autoclosure? It all comes down to the difference between a statement and an expression.

As a refresher:

An expression lets you access, modify, and assign values. In Swift, there are four kinds of expressions: prefix expressions, infix expressions, primary expressions, and postfix expressions. Evaluating an expression returns a value, causes a side effect, or both.

A statement groups expressions and controls the flow of execution. In Swift, there are three kinds of statements: simple statements, compiler control statements, and control flow statements.

Basically, an expression evaluates to a value and a statement does not. throw SomeError() is a statement — specifically, a control flow statement. It does not evaluate to a value, so we can't pass it as an autoclosure.

Maybe we could do this with an overload. Let's try this:

extension Optional {
    func unwrap(or error: @autoclosure () -> Error) throws -> Wrapped {
        guard let value = self else {
            throw error()
        }
        return value
    }

    func unwrap(or fallback: Wrapped) -> Wrapped {
        guard let value = self else {
            return fallback
        }
        return value
    }
}

So now we have the same function name, unwrap(or:), that we can call in either of these ways:

let value: String? = nil

do {
    print(value.unwrap(or: "nothing")
    try print(value.unwrap(or: ServiceError.notFound))
}
catch {
    print("Error: \(error)")
}

This works as expected. It prints:

nothing
Error: notFound

Things get weird, though, when we deal with optional errors:

let error: Error? = nil
print(error.unwrap(or: ServiceError.notFound)

This fails to compile with this error:

error: ambiguous use of 'unwrap(or:)'
print(error.unwrap(or: Fail()))
      ^

note: found this candidate
    func unwrap(or error: @autoclosure () -> Error) throws -> Wrapped {
         ^

note: found this candidate
    func unwrap(or fallback: Wrapped) -> Wrapped {
         ^

This isn't the end of the world — we generally don't deal with optional errors. But I don't like leaving this sort of rough edge on an API. So I think I'll stick with my original approach: Optional.unwrap(orThrow:) and Optional.unwrap(or:).