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?

2

There are 2 best solutions below

1
Sweeper On BEST ANSWER

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...
    }
}
2
Dávid Pásztor On

If you are working with Futures and want to chain several one off async calls after each other, you're much better off using async methods than Combine.

Unfortunately Combine has no type-level support for Publishers that only ever emit 1x and then always complete. Future tries to achieve this, but as soon as you apply any operators to a Future, you get a Publisher, so you cannot build a Combine pipeline that guarantees that each of its steps can only emit 1x.

On the other hand, async methods achieve this exact goal.

func login() async throws -> Token { ... }
func updateImage() async -> Status { ... }
func getProfile() async throws -> Profile { ... }

do {
  let token: Token
  do {
    token = try await login() 
  } catch {
    token = // assign a default value
  }
  let status = try await updateImage()
  let profile = try await getProfile()
} catch {
  // handle profile errors
}