Introduction

When a Combine URLSession.DataTaskPublisher subscription is cancelled before the network request completes, it emits a URLError.cancelled error downstream. This is different from a request failure and should typically be handled differently. Treating cancellation as an error causes unnecessary error reporting, retry attempts, and user-facing error messages for what is actually a normal lifecycle event (like a user navigating away from a screen).

Symptoms

  • URLError.cancelled logged as an error when user navigates away
  • Retry operator retries after user cancels the request
  • Error tracking service reports cancelled requests as failures
  • View disappears but network error alert still shows
  • sink(receiveCompletion:) receives .failure(.cancelled) for normal cancellation

Error output: `` Network request failed: URLError(_nsError: NSError { domain: NSURLErrorDomain, code: -999, description: "cancelled" })

Common Causes

  • Not distinguishing cancellation from actual network errors
  • Retry logic that retries cancelled requests
  • Error reporting that does not filter cancellation errors
  • View lifecycle cancelling subscriptions that are still in-flight
  • TaskGroup or async/await cancellation propagating to Combine

Step-by-Step Fix

  1. 1.Filter cancellation errors from error handling:
  2. 2.```swift
  3. 3.import Combine

class APIClient { private var cancellables = Set<AnyCancellable>()

func fetchUserData(userId: String) { URLSession.shared.dataTaskPublisher( for: URL(string: "https://api.example.com/users/\(userId)")! ) .map(\.data) .decode(type: User.self, decoder: JSONDecoder()) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { [weak self] completion in switch completion { case .finished: break case .failure(let error): // Filter out cancellation errors if (error as? URLError)?.code == .cancelled { // Normal: user navigated away, do not report return } self?.handleNetworkError(error) } }, receiveValue: { user in // Handle successful response } ) .store(in: &cancellables) }

private func handleNetworkError(_ error: Error) { // Only log/report real errors print("Network error: \(error.localizedDescription)") } } ```

  1. 1.**Use replaceError for graceful cancellation handling":
  2. 2.```swift
  3. 3.func fetchWithGracefulCancellation() -> AnyPublisher<Data, Never> {
  4. 4.URLSession.shared.dataTaskPublisher(for: url)
  5. 5..map(\.data)
  6. 6.// Replace ALL errors (including cancellation) with empty data
  7. 7..replaceError(with: Data())
  8. 8.// Or replace only cancellation errors
  9. 9..catch { error -> AnyPublisher<Data, Never> in
  10. 10.if (error as? URLError)?.code == .cancelled {
  11. 11.return Just(Data()).eraseToAnyPublisher()
  12. 12.}
  13. 13.return Fail(error: error).eraseToAnyPublisher()
  14. 14.}
  15. 15..eraseToAnyPublisher()
  16. 16.}
  17. 17.`
  18. 18.**Handle cancellation in SwiftUI view lifecycle":
  19. 19.```swift
  20. 20.struct UserDetailView: View {
  21. 21.@StateObject private var viewModel = UserDetailViewModel()
  22. 22.@State private var cancellable: AnyCancellable?

var body: some View { VStack { if let user = viewModel.user { UserProfileView(user: user) } else if viewModel.isLoading { ProgressView() } else if let error = viewModel.error { Text("Error: \(error.localizedDescription)") } } .onAppear { viewModel.loadUser() } // No need to manually cancel - @StateObject handles it } }

class UserDetailViewModel: ObservableObject { @Published var user: User? @Published var isLoading = false @Published var error: Error?

private var cancellable: AnyCancellable?

func loadUser() { isLoading = true error = nil

cancellable = URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .decode(type: User.self, decoder: JSONDecoder()) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { [weak self] completion in self?.isLoading = false if case .failure(let error) = completion { // Only set error for non-cancellation guard (error as? URLError)?.code != .cancelled else { return } self?.error = error } }, receiveValue: { [weak self] user in self?.user = user } ) }

deinit { // Cancellation happens automatically when cancellable is deallocated cancellable?.cancel() } } ```

Prevention

  • Always check for URLError.cancelled in error handling code
  • Use .ignoreFailure() or .replaceError() for non-critical requests
  • Do not retry cancelled requests
  • Do not report cancellations to error tracking services
  • Let @StateObject manage subscription lifecycle in SwiftUI
  • Use Task.isCancelled check before processing async results