Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
88: Error Handling in Combine Pipelines
Imagine you're managing a high-speed conveyor belt in a bottling plant. Everything is moving perfectly until a single bottle cracks. In a naive system, that one broken bottle triggers an emergency stop, the belt freezes, and the entire production line shuts down. To get it moving again, you have to manually reset the whole system from scratch.
That's exactly how Combine handles errors by default. When a publisher emits a .failure completion, the pipeline isn't just "paused"—it's dead. The subscription is cancelled, and no more values will ever flow through that specific stream. If you're fetching a user's profile picture and the network blips, you don't want your entire image-loading logic to stop working for the rest of the app's lifecycle. You want a way to "swap out" the broken bottle and keep the belt moving.
The Terminal Nature of Failures
I've seen a lot of developers get tripped up here. They'll set up a search bar that triggers a network request on every keystroke. The first time the API returns a 404 or a timeout, the pipeline terminates. The user keeps typing, but nothing happens. Why? Because the pipeline is gone.
// This pipeline is fragile. One error and it's over.
let searchPublisher = $searchText
.flatMap { query in
URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/search?q=\(query)")!)
.map(\.data)
.decode(type: [Result].self, decoder: JSONDecoder())
}
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Pipeline died: \(error)")
}
}, receiveValue: { results in
self.results = results
})
Providing a Safety Net with replaceError
If you don't need a complex recovery strategy and just want a "fallback" value, replaceError(with:) is your best friend. It catches any error and replaces it with a value you provide, then finishes the stream normally (sending a .finished completion instead of a .failure).
Think of this as having a backup bottle ready to go. If the main one breaks, you just slide the backup in and keep moving.
// Now, if the network fails, we just show an empty list instead of killing the stream.
let searchPublisher = $searchText
.flatMap { query in
URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/search?q=\(query)")!)
.map(\.data)
.decode(type: [Result].self, decoder: JSONDecoder())
.replaceError(with: []) // Fallback to empty array
}
.sink { self.results = $0 }
Surgical Recovery using catch
Sometimes a static value isn't enough. Maybe you want to try a different API endpoint or load data from a local cache if the network is down. This is where catch comes in. Unlike replaceError, catch allows you to return a completely new publisher.
I usually use this when I have a "primary" and "secondary" data source. It's like saying, "If the main conveyor belt breaks, immediately divert the flow to the backup belt."
.catch { error in
print("Network failed, switching to local cache...")
return LocalCache.publisher(for: query)
}
One critical detail: the catch must be placed inside the flatMap. If you put it outside, the error from the inner publisher will still propagate up and kill the main pipeline. By catching the error inside the flatMap, you're handling the failure of the request, not the failure of the stream.
Giving it Another Shot with retry
In the real world, network errors are often transient. A momentary drop in Wi-Fi shouldn't be fatal. The retry(n) operator tells Combine: "If this fails, try the whole sequence again, up to n times, before finally giving up."
I generally recommend a low number here—maybe 2 or 3. You don't want to hammer a server that's genuinely down, but a quick retry can often hide those annoying "jitter" errors from the user entirely.
URLSession.shared.dataTaskPublisher(for: url)
.retry(3) // Try 3 more times before throwing the error downstream
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())📋 Practical Task
Exercise: Building a Resilient Weather Dashboard Feed
You are building a weather app that fetches current temperatures. The API is notoriously flaky. Your goal is to create a Combine pipeline that ensures the UI always has something to display, even if the network fails.
Requirements:
- Create a publisher that simulates a network request (you can use
Fail(error: WeatherError.networkIssue).eraseToAnyPublisher()to simulate a failure). - Implement a
retrymechanism that attempts the request 2 times before failing. - Use the
catchoperator to switch to a "fallback" publisher that emits a hardcoded cached temperature (e.g., 20.0°C) if the retries also fail. - Ensure that the pipeline does not terminate, allowing subsequent triggers (like a refresh button) to start the process over again.
- Print the final temperature received in the
sink.
Bonus: Wrap the logic in a function that takes a City string and returns a publisher, ensuring the error handling happens inside the stream logic so the subscriber never receives a failure completion.
There are no comments for now.