Search code examples
swifterror-handlingcombineflatmapsink

Map Publisher to another publishers sequentially and handle errors for each step separately (or skip) in Swift Combine?


Code:

import Combine

func login() -> Future<Token, Error> { ... }
func updateImage() -> Future<Status, Never> { ... }
func getProfile() -> Future<Profile, Error> { ... }

I need to perform something like this (sequential actions):

login()
.catch { error in
  ... //handle error
}
.flatMap { token in
  ...//handle login results
  return updateImage()
}
.catch { _ in
  ... //skip error
}
.flatMap { 
  ... //handle updateImage results
  return getProfile()
}
.sink(...) //handle getProfile results and errors

The problem is Combine has misleading types inside flatMap and catch.

Tried to return Empty inside catch blocks:

return Empty<String, CustomError>(completeImmediately: true)
                    .eraseToAnyPublisher()

But I don't understand if it stops producing errors in sink section. And is it a correct approach for my task in general?


Solution

  • If you want to chain multiple of these independent Futures, and handle errors in each step, you can follow the pattern:

    future().map { result in
        // handle the future's result
        // this implicitly returns Void, turning it into a publisher of Void
    }
    .catch { error in
        // handle error...
    
        // in the case of an error,
        // if you want the pipeline to continue, return Just(())
        // if you want the pipeline to stop, return Empty()
    }
    

    Each of these is a publisher that either publishes one (), or no values at all. Then you can chain multiple of these together with flatMap:

    let cancellable = login().map { token in
        // handle login result...
        return ()
    }
    .catch { error in
        // handle login error...
        return Just(())
    }
    .flatMap { _ in
        updateImage().map { status in
            // handle updateImage results...
        }
        // no need for .catch here because updateImage doesn't fail
    }
    .flatMap { _ in
        getProfile().map { profile in
            // handle getProfile results...
        }.catch { error in
            // handle getProfile errors...
            return Just(())
        }
    }.sink { completion in
        // handle completion
    } receiveValue: { _ in
        // you will only recieve a () here
    }
    

    To help the compiler figure out the types more quickly, or even at all, you should add explicit return types and/or eraseToAnyPublisher() where appropriate.

    As Dávid Pásztor's answer said, if login and so on are async methods instead, this chaining is built directly into the language. You can write a "chain" in the same way as you write sequential statements.

    func login() async throws -> Token { ... }
    func updateImage() async -> Status { ... }
    func getProfile() async throws -> Profile { ... }
    
    func asyncChain() async {
        do {
            let token = try await login()
            // handle login results...
        } catch {
            // handle login error...
        }
        
        let status = await updateImage()
        // handle updateImage results...
        
        do {
            let profile = try await getProfile()
            // handle getProfile results...
        } catch {
            // handle getProfile error...
        }
    }