skip to Main Content

I was exploring if-case-let statements that work with Result type and do not want to use switch-case for this

Consider the following code:

import Foundation

enum CustomError: Error {
    case missingData
}

func somethingThatMightReturnError() -> Result<Data,CustomError> {
    .failure(.missingData)
}

func compute() -> Result<String,CustomError> {
    
    let computedValue = somethingThatMightReturnError()
    
    guard case Result.success(let data) = computedValue else {
        /// How to return the CustomError that is inside the computedValue ???
    }
    
    /// Here goes a lot of complex and long code that converts the Data returned as success into String which we need to return
    
    // I have force-unwrap and converted to get rid of the error
    return .success(String(data: data, encoding: .utf8)!)
}

How do I access the .failure(error) inside the guard’s else statement and return that error parameter (of .failure) from the function? Is it possible or switch-case is the only thing that works here?

2

Answers


  1. If computedValue is not success then it’s failure so simply do

    guard case Result.success(let data) = computedValue else {
        return computedValue
    }
    

    If you need to map to different success type the you can just add a dummy map inside at the guard

    return computedValue.map { _ in "" }
    

    But in this case I would consider a completely different approach that changes the function to be throwing instead

    func compute() throws -> String  {
        let computedValue = somethingThatMightReturnError()
    
        let data = try computedValue.get() //This will throw CustomError instead 
    
        // Here goes a lot of complex and long code that I do not want to put inside a switch-case
    
        return someString
    }
    
    
    
    Login or Signup to reply.
  2. Instead of restricting your approach arbitrarily to use guard, I think it is better to use the patterns that Result actually provides. I am very fond of the magical get, which turns a Result into a success returned value or a thrown error. One simple approach for your use case, if we adopt get, is to return an Error instead of a CustomError, and let the caller deal with it:

    enum CustomError: Error {
        case missingData
    }
    
    func somethingThatMightReturnError() -> Result<Data,CustomError> {
        .failure(.missingData)
    }
    
    /// This is where your complex and long conversion code goes
    func toString(data: Data) -> String { "dummy result" }
    
    func compute() -> Result<String, Error> {
        do {
            let data = try somethingThatMightReturnError().get()
            return .success(toString(data: data))
        } catch {
            return .failure(error)
        }
    }
    

    In that formulation, we do fail with the error that was thrown, but we don’t worry about its type, and we don’t need to "look inside" the incoming Result; we just "rethrow" it into the returned Result failure. You have to admit that that is beautifully compact and clear, and we didn’t use switch (and we didn’t use case let either).

    If you really really insist on returning a Result whose Error is restricted to being a CustomError, you can just bow out (not gracefully) if the incoming Error turns out not to be a CustomError:

    func compute() -> Result<String, CustomError> {
        do {
            let data = try somethingThatMightReturnError().get()
            return .success(toString(data: data))
        } catch let err as CustomError {
            return .failure(err)
        } catch {
            fatalError("this is not supposed to happen")
        }
    }
    

    Having said all of that, I would recommend that if at all possible you should abandon Result completely. It is pretty much completely replaced by Combine, and then by async/await. It isn’t 100% dead, but you are arguably spending your ingenuity in the wrong place. 🙂

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search