1

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?

Gargo
  • 1,135
  • 1
  • 10
  • 21
  • "The problem is Combine has misleading types inside flatMap and catch." What are you talking about? What are the "misleading types"? And why is this a problem? Does it produce any errors at all? – Sweeper Jun 16 '23 at 11:51
  • This might help: https://www.apeth.com/UnderstandingCombine/tricksandtips.html – matt Jun 16 '23 at 11:58
  • @Sweeper `map`/`flatMap` is ok. `catch` - how to stop executing or continue with another type like void? The official documentation shows how to use `Just` to map error to value of type specified by publisher only – Gargo Jun 16 '23 at 12:15
  • @matt thanks for a link but it doesn't include any info about how to handle error. – Gargo Jun 16 '23 at 12:22
  • It is completely unclear what your question is, so I have to guess. I was addressing your "misleading types". The catch page is https://www.apeth.com/UnderstandingCombine/operators/operatorsErrorHandlers/operatorscatch.html. I don't think you need your hand held to navigate a book so I'll leave you to it. Just click some links until you learn what you want to know. – matt Jun 16 '23 at 12:25

2 Answers2

0

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...
    }
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • it not suitable for me but at least with your help I found a mistake in my code - `catch` precedes `flatMap` so `catch` should return a publisher with the same value type as publisher obtained in the previous chain steps. – Gargo Jun 16 '23 at 13:14
-1

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
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • Though I agree that async methods are more suitable, I disagree with "so you cannot build a Combine pipeline that guarantees that each of its steps can only emit 1x" Each Combine operator is documented and does a very specific thing. Why can't it be guaranteed? Statically analyse what operators one has used, and in some cases there will be a guarantee. – Sweeper Jun 16 '23 at 12:34
  • @Sweeper documentation and compile-time guarantees that are built into the type system are very different things. Static analysis using 3rd party tools again, is a very different thing than language-level support. – Dávid Pásztor Jun 16 '23 at 12:36