Native Async/Await vs Combine for Swift Network Requests in iOS 15+
Choose URLSession.data(for:) for iOS 15+ projects or Combine for broader compatibility. Compare native async/await support, dependencies, and implementation complexity for network requests in Swift.
25 Jul 2025, 10:38 UTC

The Decision: Native Async/Await vs. Combine for Network Requests
When implementing network requests in Swift using structured concurrency, choose between native URLSession.data(for:) support and the Combine framework. This decision affects code complexity, dependency management, and iOS version compatibility.
Supported Options Compared
| Option | iOS Support | Dependencies | Complexity | Structured Concurrency |
|---|---|---|---|---|
| URLSession.data(for:) | iOS 15+ | None | Low | Native |
| Combine + URLSession | iOS 13+ | None | Medium | Requires bridging |
| Alamofire (v5.4+) | iOS 10+ | Third-party | Low | Added async/await |
Trade-offs Analysis
URLSession.data(for:) provides the cleanest integration with Swift's structured concurrency model. It returns a (Data, URLResponse) tuple that you must validate manually, but eliminates bridging overhead and external dependencies.
Combine remains viable for projects requiring iOS 13+ support, but introduces complexity when mixing with async/await. You need Task blocks to bridge publishers to async contexts.
Third-party libraries like Alamofire offer convenience but add binary size and maintenance overhead. Their async/await implementations wrap native URLSession APIs.
Concrete Implementation
Create a reusable network service using native async/await:
struct NetworkService {
func fetch(url: URL) async throws -> T {
let request = URLRequest(url: url)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(T.self, from: data)
}
}
// Usage with structured concurrency
@MainActor
class ContentViewModel: ObservableObject {
var items: [Item] = []
var isLoading = false
var error: Error?
func loadContent() async {
isLoading = true
defer { isLoading = false }
do {
items = try await NetworkService().fetch(url: URL(string: "https://api.example.com/items")!)
} catch {
self.error = error
}
}
}
Validation Steps
- Project Setup: Create an Xcode project with deployment target iOS 15.0 or later.
- Compile Check: Build the project and verify no warnings from async/await syntax.
- Runtime Test: Run in the simulator with Network Link Conditioner set to "Very Bad Network" to test timeout handling.
- Cancellation Test: Wrap multiple concurrent requests in
withTaskGroupand cancel the parent task to verify cleanup.
Limitations and Considerations
URLSession.data(for:) requires iOS 15+, meaning projects supporting iOS 13 or 14 must use @available checks or adjust deployment targets. The API returns raw data requiring manual decoding and response validation. Actor isolation rules may require @MainActor annotations when updating UI state after network completion.
Practical Verification
To verify your implementation handles errors correctly:
func testNetworkErrorHandling() async {
do {
let service = NetworkService()
_ = try await service.fetch(url: URL(string: "https://invalid-url.example.com/data")!)
} catch let error as URLError {
print("Expected network error: \(error.code.rawValue)")
} catch {
print("Unexpected error type: \(error)")
}
}
This approach gives you predictable error handling and clear cancellation semantics through Swift's structured concurrency model.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.