[Swift] Perform error processing with Result <Success, Failure> type

Error handling by Result \ <Success, Failure> type

In error handling by Optional \ type, The presence or absence of a value determines success or failure.

On the other hand, in error handling of Result \ <Success, Failure> type, ** Success is represented by the resulting value and failure is represented by the error details. ** **

That is, the Result \ <Success, Failure> type is You can overcome the disadvantage of not being able to represent the details of ** Optional \ type errors. ** **

Implementation method

The Result \ <Success, Failure> type is an enumeration that takes two type arguments. It has two cases, .success and .failure.

The enumeration type Success represents the type of the value at the time of success Failure represents the type of error at the time of failure.


public enum Result<Success, Failure> where Fairule : Error {
    case success(Success)   // .In case of success, it has a success type associative value.
    case success(Failure)   // .In case of failure, it has a Failure type associative value.
}

It seems that the Swift standard library has an Error protocol that indicates an error. Because the type argument Failure of type Result \ <Success, Failure> represents an error It seems that it needs to comply with the Error protocol.

The procedure is actually error handling, The Result \ <Success, Failure> type will be specified in the return value of the function.

But before that, you need to define ** Success and Failure. ** ** Success is the value returned on success, Failure defines the error content to be returned on failure.


// Failure
enum DatabaseError : Error {
    case entryNotFound   //Data not found error
    case duplicatedEntry   //Error due to duplicate data
    case invalidEntry(reason: String)   //Error due to incorrect data
}

// Success
struct User {
    var id: Int
    var name: String
}

Next, define the function findUser (byID :) that retrieves the user by ID. If found, enter a User type value, If not found, returns an entryNotFound case of type DatabaseError.


let user1 = User(id: 1, name: "Yamaguchi")
let user2 = User(id: 2, name: "Tanaka")

var users = [user1, user2]

func findUser(byID id: Int) -> Result<User, DatabaseError> {
    for user in users {
        if user.id == id {
            return .success(user)
        }
    }
    return .failure(.entryNotFound)
}

The findUser (byID :) function is being executed by passing 0 as an argument.

Since the return value of the function goes into result, Retrieves a value with a value binding pattern.


let id = 0
let result = findUser(byID: id)

switch result {
case let .success(user):   //If the return value is success
    print(".success: \(user)")
case let .failure(error):   //If the return value is failure
    switch error {   //Extract the contents of failure
    case .duplicatedEntry:
        print(".failure: .duplicatedEntry")
    case .entryNotFound:
        print(".failure: .entryNotFound")
    case .invalidEntry(let reason):
        print(".failure: .invalidEntry(\(reason))")
    }
}

Putting it all together, it looks like this.


enum DatabaseError : Error {
    case entryNotFound
    case duplicatedEntry
    case invalidEntry(reason: String)
}

struct User {
    var id: Int
    var name: String
}

let user1 = User(id: 1, name: "Yamaguchi")
let user2 = User(id: 2, name: "Tanaka")

var users = [user1, user2]

func findUser(byID id: Int) -> Result<User, DatabaseError> {
    for user in users {
        if user.id == id {
            return .success(user)
        }
    }
    return .failure(.entryNotFound)
}

let id = 0
let result = findUser(byID: id)

switch result {
case let .success(user):
    print(".success: \(user)")
case let .failure(error):
    switch error {
    case .duplicatedEntry:
        print(".failure: .duplicatedEntry")
    case .entryNotFound:
        print(".failure: .entryNotFound")
    case .invalidEntry(let reason):
        print(".failure: .invalidEntry(\(reason))")
    }
}

Execution result
.failure: .entryNotFound

This time, the result is only .failure: .entryNotFound, If you want to change the error depending on the processing in the function, you can write it like this!


enum DatabaseError : Error {
    case entryNotFound   //Data not found error
    case duplicatedEntry   //Error due to duplicate data
}

struct User {
    var id: Int
    var name: String
}

let user1 = User(id: 1, name: "Yamaguchi")
let user2 = User(id: 2, name: "Tanaka")
let user3 = User(id: 2, name: "Takeda")
let user4 = User(id: 4, name: "Akimoto")

var users = [user1, user2, user3, user4]

func findUser(byID id: Int) -> Result<User, DatabaseError> {
    var userCount = 0
    var userInfo: User?
    
    for user in users {
        if user.id == id {
            userCount += 1
            userInfo = user
        }
    }
    if userCount == 0 {
        return .failure(.entryNotFound)
    } else if userCount >= 2 {
        return .failure(.duplicatedEntry)
    }
    
    return .success(userInfo!)
}

let idArray = [1,2,3,4]
for id in idArray {
    let result = findUser(byID: id)
    switch result {
    case let .success(user):
        print(".success: \(user)")
    case let .failure(error):
        switch error {
        case .duplicatedEntry:
            print(".failure: .duplicatedEntry")
        case .entryNotFound:
            print(".failure: .entryNotFound")
        }
    }
}

Execution result
.success: User(id: 1, name: "Yamaguchi")
.failure: .duplicatedEntry
.failure: .entryNotFound
.success: User(id: 4, name: "Akimoto")

From the execution result Since there are two users with id: 2, a duplicate error will occur. Since id: 3 does not exist, an unknown data error will occur. For id: 1 and 4, the result will be displayed because there is a corresponding user!

There may have been a more concise way to write it, but this was the only way I could make it right away ...

Usage timing

** Provide error details **

Unlike the Optional \ type, the Result \ <Success, Failure> type Returns the value of the error on failure through the associative value.

** Therefore, when an error occurs, you can always receive the details of the error. And you can control the error handling according to the details. ** **

The error handling control is to retry several times if there is a communication error. If there is an error on the server side, it is an operation such as displaying an alert without retrying.

Also, the content of the error can be defined here, so ** It is possible to understand the details of possible errors here. ** **

In situations like this where you need to provide error details, I think it's better to use the Result \ <Success, Failure> type.

Supplement

Is it possible to handle errors using closures like this? I thought, so I will add it.


enum SomeError: Error {
    case error(reason: String)
    case warning(reason: String)
    
}

struct User {
    var name: String
    var age: Int
    var income: Int
}

func checkStatus(name: String, age: Int, income: Int, result: (User?, SomeError?) -> Void) {
    if age >= 18 && income <= 250 {
        result(nil, .warning(reason: "\(name)His annual income is a little lower than the regulation."))
    } else if age < 18 {
        result(nil, .error(reason: "\(name)Is under 18 years old."))
    }
    
    result(User(name: name, age: age, income: income), nil)
}

checkStatus(name: "Tanabe", age: 18, income: 300) { (user, error) in
    guard error == nil else {
        print(error!)
        return
    }
    
    print(user!)
}

Execution result
User(name: "Tanabe", age: 18, income: 300)

This concludes the explanation of error handling performed by the Result <Success, Failure> type.

To be honest, it's difficult ...! I thought I had to get used to it, so I will actively define error handling.

Error handling is absolutely essential, so please make it available to everyone!

-[Swift] Perform error handling with Optional \ type[Swift] Do-catch handles errors (Part 1) -[Swift] Termination of program by fatalError function[Swift] Termination of program by assertion

Thank you for watching until the end.

Recommended Posts

[Swift] Perform error processing with Result <Success, Failure> type
[Swift] Perform error handling with Optional <Wrapped> type
[Swift] Perform error handling with do-catch statement (Part 3)
[Swift] Perform error handling with do-catch statement (Part 2)
[Swift] Perform error handling with do-catch statement (Part 1)
[Swift / For beginners] Write smartly with type inference
error code 400 appears when deploying with release: perform
I want to perform aggregation processing with spring-batch