Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice for Swift methods that can return or error [closed]

Tags:

swift

I’m practicing Swift and have a scenario (and a method) where the result could either be successful or a failure.

It’s a security service class. I have a method where I can authenticate with an email address and password, and want to either return a User instance if the credentials are correct, or throw some form of false value.

I’m a bit confused as my understanding of Swift methods is you need to specify a return type, so I have:

class SecurityService {
    static func loginWith(email: String, password: String) -> User {
        // Body
    }
}

I’ve seen in Go and Node.js methods that return a “double” value where the first represents any errors, and the second is the “success” response. I also know that Swift doesn’t have things like errors or exceptions (but that may have changed since as I was learning an early version of Swift).

What would be the appropriate thing to do in this scenario?

like image 536
Martin Bean Avatar asked Sep 09 '16 15:09

Martin Bean


4 Answers

If you want to handle errors that can happen during login process than use the power of Swift error handling:

struct User {
}

enum SecurityError: Error {
    case emptyEmail
    case emptyPassword
}

class SecurityService {
    static func loginWith(email: String, password: String) throws -> User {
        if email.isEmpty {
            throw SecurityError.emptyEmail
        }
        if password.isEmpty {
            throw SecurityError.emptyPassword
        }
        return User()
    }    
}

do {
    let user = try SecurityService.loginWith1(email: "", password: "")
} catch SecurityError.emptyEmail {
    // email is empty
} catch SecurityError.emptyPassword {
    // password is empty
} catch {
    print("\(error)")
}

Or convert to optional:

guard let user = try? SecurityService.loginWith(email: "", password: "") else {
    // error during login, handle and return
    return
}

// successful login, do something with `user`

If you just want to get User or nil:

class SecurityService {    
    static func loginWith(email: String, password: String) -> User? {
        if !email.isEmpty && !password.isEmpty {
            return User()
        } else {
            return nil
        }
    }
}

if let user = SecurityService.loginWith(email: "", password: "") {
    // do something with user
} else {
    // error
}

// or

guard let user = SecurityService.loginWith(email: "", password: "") else {
    // error
    return
}

// do something with user
like image 163
mixel Avatar answered Sep 27 '22 18:09

mixel


Besides the standard way to throw errors you can use also an enum with associated types as return type

struct User {}

enum LoginResult {
  case success(User)
  case failure(String)
}

class SecurityService {
  static func loginWith(email: String, password: String) -> LoginResult {
    if email.isEmpty { return .failure("Email is empty") }
    if password.isEmpty { return .failure("Password is empty") }
    return .success(User())
  }
}

And call it:

let result = SecurityService.loginWith("Foo", password: "Bar")
switch result {
  case .Success(let user) : 
     print(user) 
     // do something with the user
  case .Failure(let errormessage) : 
     print(errormessage) 
    // handle the error
}
like image 25
vadian Avatar answered Sep 27 '22 19:09

vadian


Returning a result enum with associated values, throwing exception, and using a callback with optional error and optional user, although valid make an assumption of login failure being an error. However thats not necessarily always the case.

  • Returning an enum with cases for success and failure containing result and error respectively is almost identical as returning an optional User?. More like writing a custom optional enum, both end up cluttering the caller. In addition, it only works if the login process is synchronous.
  • Returning result through a callBack, looks better as it allows for the operation to be async. But there is still error handling right in front of callers face.
  • Throwing is generally preferred than returning an error as long as the scope of the caller is the right place to handle the error, or at least the caller has access to an object/method that can handle this error.

Here is an alternative:

func login(with login: Login, failure: ((LoginError) -> ())?, success: (User) -> ()?) {
    if successful {
        success?(user)
    } else {
        failure?(customError)
    }
}

// Rename with exactly how this handles the error if you'd have more handlers, 
// Document the existence of this handler, so caller can pass it along if they wish to.
func handleLoginError(_ error: LoginError) {
    // Error handling
}

Now caller can; simply decide to ignore the error or pass a handler function/closure.

login(with: Login("email", "password"), failure: nil) { user in 
    // Ignores the error 
}

login(with: Login("email", "password"), failure: handleLoginError) { user in 
    // Lets the error be handled by the "default" handler.
} 

PS, Its a good idea to create a data structure for related fields; Login email and password, rather individually setting the properties.

struct Login {
    typealias Email = String 
    typealias Password = String 

    let email: Email
    let password: Password
}
like image 31
Lukas Avatar answered Sep 27 '22 18:09

Lukas


To add an answer to this question (five years later), there’s a dedicated Result type for this exact scenario. It can return the type you want on success, or type an error on failure.

It does mean re-factoring some code to instead accept a completion handler, and then enumerating over the result in that callback:

class SecurityService {
    static func loginWith(email: String, password: String, completionHandler: @escaping (Result<User, SecurityError>) -> Void) {
        // Body
    }
}

Then in a handler:

securityService.loginWith(email: email, password: password) { result in
    switch result {
    case .success(let user):
        // Do something with user
        print("Authenticated as \(user.name)")
    case .failure(let error):
        // Do something with error
        print(error.localizedDescription)
    }
}
like image 33
Martin Bean Avatar answered Sep 27 '22 20:09

Martin Bean