iOS

Combine - Error Handling

728x90

Error handling with mapError, setFailureType, & flatMap

Combine의 Publisher에서 Failure가 Never이지만, operator에서 Error을 처리해줘야하는 경우가 있을 수가 있다. 아래처럼 2가지 방법이 존재한다.

  • Failure를 변환함.
  • Error을 잘 처리해서, Failure가 없는 Never 타입으로 변경함.

샘플 코드를 통해 어떻게 catch, retry, setFailureType, replaceError(with), mapError를 살펴보자

샘플 코드는 url를 통해 이미지를 가져오는 예제이다.

시나리오는 아래와 같다.

  • URL 주소 값은 string 타입의 FailureType이 Never인 Publisher이다.
    1. URL이 변경 될 때 이미지를 호출한다.
    2. 호출 된 이미지는 FailureType이 Never인 Publisher에 값을 보내준다.
  • 이슈: url 주소값과 호출되는 이미지의 Publisher의 FailureType은 Never인데, URLSession 통신은 에러가 발생 할 수 밖에 없다.(Network연결이 안되어있거나, 이미지 서버가 내려갔거나, url 주소가 변경되서 동작이 안되거나 등등)
    • 물론, 아무런 에러 처리를 안해주면 ios 입장에선 Failure가 Never이기 때문에 정상적으로 동작하지만 Error가 발생한다면 앱은 종료 되지 않을까?

그럼 에러 처리 방법을 알아보자.

  • Catch
    • Error가 발생 했을 시, 어떤 Publisher를 전달해줄지 설정할수 잇음. Empty()를 전달하면 빈 Publisher을 전달해주게됨
  • retry
    • 실패, Error가 발생 했을 경우 몇번 더 요청을 할 것인지 설정 할 수 있음.
  • setFailureType
    • publisher의 Error가 Never이더라도, setFailureType을 통해 publisher의 Error Type을 변경해줄 수 있음.
    • publisher의 operator 중 URLSession.shared.dataTaskPublisher 있다면, Error가 발생 하는 Publisher임. 이럴 때 얘를 사용해서 URLError가 발생하는 Publisher로 변경이 가능함.
  • replaceError(with)
    • Error가 발생 했을 경우 어떤 것을 Publisher값으로 반환해줄지 지정가능.
    • 지정을 해주었다면, Publisher Error는 Never로 설정됨.
  • mapError
    • 발생된 에러를 캐치하여, URLError인지, APIError인지 어떤 Error인지 식별 할 때 사용 할 수 있음.
    • Error의 유형을 원하는대로 구분지어 변경 할 수 있음.

이제 사용법을 살펴보자.

Catch

먼저 catch가 제일 쉽다.

기본 문법

struct SimpleError: Error {}
let numbers = [5, 4, 3, 2, 1, 0, 9, 8, 7, 6]
cancellable = numbers.publisher
    .tryLast(where: {
        guard $0 != 0 else {throw SimpleError()}
        return true
    })
    .catch({ (error) in
        Just(-1)
    })
    .sink { print("\\($0)") }
    ****// Prints: -1

예제 문법

아래처럼 Empty()를 통해 빈 Publisher을 전달 해 줄 수 있다.

.map{ (url) in
      URLSession.shared.dataTaskPublisher(for: url)
          .map(\\.data)
          .compactMap{
              UIImage(data: $0)
          }
          .catch { _ in
              // 에러 발생시 빈 Publisher로
              Empty()
          }
  }

setFailureType

  • error type을 변경 할 수 있음.

기본문법

let pub1 = [0, 1, 2, 3, 4, 5].publisher
let pub2 = CurrentValueSubject<Int, Error>(0)
let cancellable = pub1
    .setFailureType(to: Error.self)
    .combineLatest(pub2)
    .sink(
        receiveCompletion: { print ("completed: \\($0)") },
        receiveValue: { print ("value: \\($0)")}
     )

// Prints: "value: (5, 0)".

예제

// Publihser의 ErrorType을 변경해줌.
.setFailureType(to: URLError.self)
.map{ (url) in
      URLSession.shared.dataTaskPublisher(for: url)
          .map(\\.data)
          .compactMap{
              UIImage(data: $0)
          }
          .catch { _ in
              Empty()
          }
  }

replaceError(with)

  • error가 발생 했을 때, 대치 할 값을 설정할 수 있음.

기본문법

struct MyError: Error {}
let fail = Fail<String, MyError>(error: MyError())
cancellable = fail
    .replaceError(with: "(replacement element)")
    .sink(
        receiveCompletion: { print ("\\($0)") },
        receiveValue: { print ("\\($0)", terminator: " ") }
    )

// Prints: "(replacement element) finished".

예제

.map{ (url) in
      URLSession.shared.dataTaskPublisher(for: url)
          .map(\\.data)
          .compactMap{
              UIImage(data: $0)
          }
          // error가 발생 했을 경우, 내부의 asset에 있는 사진으로 변경함. 
				  .replaceError(with: UIImage(named: "pink")!)
  }

mapError

  • error 타입을 변경해서 전달함.

기본문법

struct DivisionByZeroError: Error {}
struct MyGenericError: Error { var wrappedError: Error }

func myDivide(_ dividend: Double, _ divisor: Double) throws -> Double {
       guard divisor != 0 else { throw DivisionByZeroError() }
       return dividend / divisor
   }

let divisors: [Double] = [5, 4, 3, 2, 1, 0]
divisors.publisher
    .tryMap { try myDivide(1, $0) }
    .mapError { MyGenericError(wrappedError: $0) }
    .sink(
        receiveCompletion: { print ("completion: \\($0)") ,
        receiveValue: { print ("value: \\($0)", terminator: " ") }
     )

// Prints: "0.2 0.25 0.3333333333333333 0.5 1.0 completion: failure(MyGenericError(wrappedError: DivisionByZeroError()))"

예제

func fetch(url: URL) -> AnyPublisher<Data, APIError> {
      return URLSession.shared.dataTaskPublisher(for: url)
          .tryMap({( data , response ) -> Data in
              if let response = response as? HTTPURLResponse,
                 !(200...299).contains(response.statusCode) {
                  throw APIResources.APIError.badResponse(statusCode: response.statusCode)
              } else {
                  return data
              }
          })
          .mapError({ error in
              // error 타입을 변경해서 전달함.
              APIResources.APIError.convert(error: error)
          })
          .eraseToAnyPublisher()
  }
enum APIError: Error, CustomStringConvertible {
        
        case url(URLError?)
        case badResponse(statusCode: Int)
        case unknown(Error)
        
        static func convert(error: Error) -> APIError {
            switch error {
            case is URLError:
                return .url(error as? URLError)
            case is APIError:
                return error as! APIError
            default:
                return .unknown(error)
            }
        }
        
        var description: String {
            return ""
        }
    }
반응형