Github.com/agelessman/…
This article focuses on how to handle errors in Pipline.
catch
The core usage of catch is clearly expressed in the figure above. From a macro perspective, it catches an exception sent from publisher and returns to another publisher. The purpose of the new Publisher is to replace the old publisher, and the Pipline continues to execute.
Next, let’s take a closer look at catch with three different usage scenarios.
The first:
enum MyError: Error {
case custom
}
let publisher = PassthroughSubject<String.Error>()
publisher
.catch { err in
Just("!!!")
}
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
publisher.send("You")
publisher.send("Good")
publisher.send(completion: Subscribers.Completion.failure(MyError.custom))
publisher.send("吗")
Copy the code
It is the above code that generates the diagram in this section. We chose PassthroughSubject as publisher to facilitate sending data. As you can see, when we send a failure event, a catch will be triggered, and the catch will receive a closure. The closure returns a new Publisher, and in the code above we return a Just(“!!!” ). So the end result is that the old Publisher is cancelled and the new Just executes, but Just sends data once and ends up.
Therefore, the print is as follows:
You are good!!!!!!!!!
finished
Copy the code
Second, we use the official example:
[1.2.3.0.5]
.publisher
.tryLast {
guard $0 ! = 0 else { throw MyError.custom }
return true
}
.catch { err in
Just(-1)
}
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
Copy the code
We use tryLast to fetch the last data in the pipline, and the code above is a bit confusing:
.tryLast {
guard $0 ! = 0 else { throw MyError.custom }
return true
}
Copy the code
TryLast will wait for the publisher to send the data, iterate through the data, and return the last value of the closure with a result of true.
The closure is invoked each time the data is traversed, and when the convenience reaches zero, an exception is raised. The code above prints:
-1
finished
Copy the code
Third, we will use. Catch to return publisher. In Combine, flatMap is used for data mapping, its closure requires a pulisher to return, and the code below will use. Catch and flatMap in combination.
let testURL = URL(string: "https://xxxx.com")!
Just(testURL)
.flatMap {
URLSession.shared.dataTaskPublisher(for: $0)
.tryMap { data, response -> Data in
guard let httpResp = response as? HTTPURLResponse, httpResp.statusCode = = 200 else {
throw NetworkError.invalidResponse
}
return data
}
.decode(type: Student.self, decoder: JSONDecoder())
.catch { err in
Just(Student(name: "", age: 0))
}
}
.eraseToAnyPublisher()
Copy the code
The above code is more commonly used, in the case of network request error, we return the default data.
Catch has an extension called tryCatch, which allows us to actively throw exceptions. An official example is to replace weak network urls if the network is limited, as follows:
func adaptiveLoader(regularURL: URL.lowDataURL: URL) -> AnyPublisher<Data.Error> {
var request = URLRequest(url: regularURL)
request.allowsConstrainedNetworkAccess = false
return URLSession.shared.dataTaskPublisher(for: request)
.tryCatch { error -> URLSession.DataTaskPublisher in
guard error.networkUnavailableReason = = .constrained else {
throw error
}
return URLSession.shared.dataTaskPublisher(for: lowDataURL)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPUrlResponse,
httpResponse.statusCode = = 200 else {
throw MyNetworkingError.invalidServerResponse
}
return data
}
.eraseToAnyPublisher()
Copy the code
As you can see, if the cause of the network error is not a network constraint, end the Pipline with an error, if it is a network constraint, use a weak network request.
assertNoFailure
AssertNoFailure is used primarily for test purposes, and once the pipline in test is sent.failure, an error will be emitted, which is not much to be said.
retry
As shown in the figure above, Retry enables resubscription of publisher when an error is sent from upstream publisher.
Its main usage scenario is to retry a network request, the code is as follows:
func loadData(a) {
let myURL = URL(string: "https://www.example.com")
URLSession.shared.dataTaskPublisher(for: myURL!)
.retry(3)
.map({ (page) -> WebSiteData in
return WebSiteData(rawHTML: String(decoding: page.data, as: UTF8.self))
})
.catch { error in
return Just(WebSiteData(rawHTML: "<HTML>Unable to load page - timed out.</HTML>"))
}
.assign(to: \.data, on: self)
.store(in: &cancellables)
}
Copy the code
mapError
The image above is just a demonstration of the core idea of mapError. In general, pipline encounters an error and terminates immediately.
In real development, there are all kinds of errors, which can be caused by network or other errors. The core function of mapError is to map errors in pipline.
For a simple example, if we encapsulate a network framework like Alamofire ourselves, we would need to identify various errors in the network request process, as Alamofire has a special AFError. Swift file for Error resolution. So in Pipline, our goal is to map other errors to our custom error type, as follows:
.mapError{ error -> MyApiError in
if let err = error as? MyApiError {
return err
}
if let urlErr = error as? URLError {
return MyApiError.invalidURL(urlError: urlErr)
}
return MyApiError.unknown
}
Copy the code