Pitch: Switch on error

Hi there,

I encountered a use case lately where I was iterating over an async sequence and I wanted to fallback to another sequence in case of a failure.

Kotlin Flow can do that:

val base = flowOf { ... }
val other = flowOf { ... }
val flow = flowOf(base).catch { emitAll(other) }

We could have an AsyncSwitchOnErrorSequence + an extension on AsyncSequence to create that AsyncSwitchOnErrorSequence.

The implementation would be something like that:

extension AsyncSequence {
  public func switchOnError<Other: AsyncSequence>(
    _ other: Other
  ) -> AsyncSwitchOnErrorSequence<Self, Other> {
    AsyncSwitchOnErrorSequence(base: self, other: other)
  }
}

public struct AsyncSwitchOnErrorSequence<Base: AsyncSequence, Other: AsyncSequence>: AsyncSequence
where Base.Element == Other.Element {
  public typealias AsyncIterator = Iterator
  public typealias Element = Base.Element

 private let base: Base
  private let other: Other

  init(base: Base, other: Other) {
    self.base = base
    self.other = other
  }

  public func makeAsyncIterator() -> AsyncIterator {
    Iterator(
      baseIterator: base.makeAsyncIterator(),
      otherIterator: other.makeAsyncIterator()
    )
  }

  public struct Iterator: AsyncIteratorProtocol {

    var baseIterator: Base.AsyncIterator
    var otherIterator: Other.AsyncIterator
    var baseHasFailed = false

    public mutating func next() async throws -> Element? {
      guard !Task.isCancelled else { return nil }

      guard !baseHasFailed else {
        return try await otherIterator.next()
      }

      let element: Element?

      do {
        element = try await baseIterator.next()
      } catch {
        baseHasFailed = true
        element = try await otherIterator.next()
      }

      return element
    }
  }
}

extension AsyncSwitchOnErrorSequence: Sendable where Base: Sendable, Other: Sendable { }

Would this be interesting ?

1 Like

@FranzBusch @Philippe_Hausler do you have an opinion on that ?

Leaving name bike shedding aside I think that functionality is very useful. It could make sense to provide closure that can inspect the error and then take a decision if we should switch or not. Since this is so closely tied to the failure of an AsyncSequence it might make sense to wait until the primary associated types for AsyncSequences have shipped with a Swift version.

This is similar to onErrorJustError / catchErrorJustReturn operator in Rx and it is useful in practice. See ReactiveX - Operators

1 Like

I would agree that this seems like something that would be useful; there are similar thinks as mentioned in Rx, but also similar design patterns in Combine too. So the prior art is there.

I agree with @FranzBusch that this should definitely dovetail with typed throws. This however seems suspiciously similar to some sort of catch + flatMap.

1 Like