Skip to content

Custom Combine Operators (Swift)

A SwiftUI guide for coding agents. Also covers custom publisher operator, publisher extension, catch to result, finish on fail, never failing side effect, combine error handling helpers.

New Combine operators do not need a hand-written Publisher conformance. Compose existing operators inside a Publisher extension and erase the type. Three operators cover the error handling most apps repeat.

replaceErrorOrEmpty

replaceEmpty and replaceError almost always travel together; fuse them.

extension Publisher {
  func replaceErrorOrEmpty(with output: Output) -> AnyPublisher<Output, Never> {
    self
      .replaceEmpty(with: output)
      .replaceError(with: output)
      .eraseToAnyPublisher()
  }
}

finishOnFail

Sometimes a failure should just end the stream silently, such as a remote refresh merged with cached data: the cache already covers the failure case.

extension Publisher {
  func finishOnFail() -> AnyPublisher<Output, Never> {
    self
      .catch { _ in Empty() }
      .eraseToAnyPublisher()
  }
}

Publishers.Merge(
  newsService.fetchCachedNews(),
  newsService.fetchRemoteNews().finishOnFail()
)
.map(Action.setNews)

catchResult

When the error belongs in app state, wrap value and failure into one Result so the publisher itself never fails.

extension Publisher {
  func catchResult() -> AnyPublisher<Result<Output, Failure>, Never> {
    self
      .map(Result.success)
      .catch { Just(Result.failure($0)) }
      .eraseToAnyPublisher()
  }
}

In a Redux-style store this is the natural shape for side effects: a reducer's effect must never fail, so a failure becomes an ordinary action carrying Result.failure.

case .fetchAuth:
  return environment.service
    .authorize()
    .catchResult()
    .map(Action.setAuth)
    .eraseToAnyPublisher()
case .setAuth(let result):
  state.auth = result
  return Empty().eraseToAnyPublisher()

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-building-custom-combine-operators-in-swift" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems