Quick Verdict
Use async/await for single-response work like network requests, database queries, and file I/O. Use Combine for values over time like search debouncing, form validation, and multi-stream coordination. Combine is not deprecated and still underpins SwiftData, but Apple's investment has clearly shifted to Swift Concurrency. New projects should default to async/await and reach for Combine only when reactive operators genuinely simplify the problem. See our async/await networking tutorial for practical examples.
The Confusion Is Understandable
Apple introduced Combine in 2019 as its reactive programming framework. Two years later, at WWDC 2021, they introduced async/await and structured concurrency. Since then, the community has been trying to figure out whether Combine is being replaced, deprecated, or just... ignored.
The answer is nuanced, and that is part of the problem. Apple has not deprecated Combine. It still underpins parts of SwiftData and other system frameworks. But the last meaningful update to Combine was the .values property added in 2021, which bridges Combine publishers into AsyncSequence. That was three years ago, and the silence says a lot.
Meanwhile, Swift Concurrency has received major updates at every WWDC since 2021. Swift 6.2 at WWDC 2025 brought further concurrency improvements to ease adoption pain points. The trajectory is clear, even if the deprecation notice is not.
What Each One Actually Does
Before comparing them, let us be precise about what problem each tool solves.
Async/Await (Swift Concurrency)
Async/await is a language-level feature for writing asynchronous code that reads like synchronous code. You mark a function as async, call it with await, and the compiler handles suspension and resumption. No callbacks, no nested closures, no retain cycle landmines.
The broader Swift Concurrency system includes Task, TaskGroup, actors, AsyncSequence, AsyncStream, and the Swift Async Algorithms package. Together, these cover structured concurrency (tasks with parent-child relationships), unstructured concurrency (fire-and-forget tasks), and data streams.
Combine
Combine is a reactive framework built on the publisher-subscriber pattern. A publisher emits values over time. Subscribers receive those values. Between them, you chain operators like map, filter, debounce, combineLatest, and switchToLatest to transform, merge, and control the flow of data.
Combine's mental model is declarative data pipelines. You describe how data flows through your system, and the framework handles the execution. It is powerful for complex, multi-stream reactive scenarios where you need fine-grained control over timing and composition.
The Simple Decision Rule
Here is the rule that will be correct 80% of the time:
If data returns once, use async/await. If data emits multiple values over time, consider Combine.
That single heuristic covers most real-world decisions. A network request that returns a single response? Async/await. A text field that emits keystrokes you want to debounce? Combine. A database query that returns once? Async/await. A WebSocket that emits messages continuously? Either, but Combine's operators make it elegant.
Detailed Comparison Table
| Factor | Async/Await | Combine |
|---|---|---|
| Introduced | WWDC 2021 (Swift 5.5) | WWDC 2019 (iOS 13) |
| Type | Language feature (built into Swift) | Framework (import Combine) |
| Mental Model | Sequential, "do this then that" | Declarative pipelines, "data flows through" |
| Best For | Single async operations, linear flows | Multi-value streams, reactive UI binding |
| Error Handling | try/catch (familiar, linear) | .failure case on publishers (functional) |
| Cancellation | Task.cancel(), cooperative | AnyCancellable (automatic on dealloc) |
| Debounce/Throttle | Via Swift Async Algorithms package | Built-in operators |
| Combine Multiple Streams | TaskGroup, AsyncSequence merge | combineLatest, merge, zip (mature) |
| Learning Curve | Low to moderate | Moderate to steep |
| Apple Investment (2024-2026) | Heavy (updates every WWDC) | Minimal (no updates since 2021) |
| Swift 6 Compatibility | Designed for it | Challenging (isolation issues) |
When Async/Await Wins Clearly
For the following scenarios, async/await is the better tool. Using Combine here would add complexity for no benefit.
Network Requests
This is the most common case. You call an API, wait for a response, decode the JSON, and update your UI. Async/await makes this a linear, readable flow. No publishers, no sinks, no AnyCancellable storage.
With Combine, the same operation requires creating a publisher from URLSession, chaining .decode(), .receive(on: DispatchQueue.main), and .sink(), then storing the cancellable. It works, but it is more code with more cognitive overhead for a single-response operation. Check our networking with async/await guide for the full pattern.
Database Queries
Fetching data from SwiftData, Core Data, or Supabase is a single async operation. You send a query, you get results. Async/await fits naturally.
File Operations
Reading or writing files, processing images, or performing any I/O-bound work that returns once. The .task modifier in SwiftUI is perfect for kicking off this kind of work when a view appears.
Sequential Multi-Step Flows
"Authenticate the user, then fetch their profile, then load their settings." Async/await handles sequential dependencies beautifully because the code reads top-to-bottom, exactly as it executes. With Combine, you would chain .flatMap operators, which is harder to read and debug.
When Combine Still Wins
There are scenarios where Combine's operator library genuinely simplifies your code. The Swift Async Algorithms package is closing this gap, but Combine still has the edge in these areas.
Search Debouncing
The classic example. A user types in a search field and you want to wait 300ms after they stop typing before firing a network request. With Combine, this is elegant: $searchText.debounce(for: .milliseconds(300), scheduler: RunLoop.main). The Swift Async Algorithms package now offers an equivalent debounce operator for AsyncSequence, but the Combine version is more established and battle-tested.
Form Validation with Multiple Inputs
When you need to combine the latest values from multiple form fields and produce a single "is valid" signal, combineLatest is purpose-built for this. You can achieve the same with computed properties in an @Observable class (and for most forms, you should), but when validation involves async checks like username availability, Combine's operator chaining is cleaner.
Real-Time Event Streams
WebSockets, Bluetooth data, sensor readings, or any source that emits values continuously over time. Combine's publisher model maps naturally to this pattern. You can also use AsyncStream for this, and many developers are migrating to it. But if you need to merge multiple streams, apply backpressure, or use operators like switchToLatest, Combine's operator set is richer.
Timer-Based UI Updates
Timer.publish(every:on:in:) is a clean way to drive periodic UI updates. Yes, you can write an async loop with Task.sleep, but the Combine version integrates with RunLoop and is more explicit about its lifecycle.
Is Combine Deprecated?
No. Not officially. But the writing is on the wall, and it is worth being honest about what that means for your codebase.
Apple has not added a single new API to Combine since the .values property in iOS 15 (2021). That is four years of silence. Meanwhile, Swift Concurrency gets significant updates at every WWDC. At WWDC 2025, Swift 6.2 introduced further concurrency refinements, and Apple's "Embracing Swift Concurrency" session made the direction clear.
More practically, using Combine within strict Swift 6 concurrency contexts is becoming difficult. Combine's types were not designed for the isolation model that Swift 6 enforces. Running Combine pipelines outside of @MainActor contexts introduces potential pitfalls that did not exist before. This friction will only increase.
The pragmatic take: Combine works, it is not going away tomorrow, and it is fine to use where it makes sense. But for new code, default to async/await and @Observable. Reach for Combine only when you genuinely need reactive operators that have no good Swift Concurrency equivalent.
The Modern SwiftUI Approach: Using Both
The best SwiftUI apps in 2026 are not dogmatic about using one or the other. They use async/await as the default and Combine where it genuinely simplifies the problem. Here is what that looks like in practice, following good MVVM architecture patterns.
Your ViewModel Layer
Use @Observable (the new observation framework) instead of ObservableObject with @Published. This eliminates most of the reason people used Combine in ViewModels. Properties update views automatically without needing Combine publishers.
For async work, call async functions directly from your ViewModel methods. No Combine pipeline needed. The view triggers the work with .task or a button action that launches a Task.
Your Service Layer
Network services, database services, and API clients should expose async functions. This is the natural fit for single-response operations that make up 90% of service layer work.
For services that emit ongoing events (WebSocket connections, Bluetooth managers, location updates), return an AsyncStream or AsyncSequence. This is the modern replacement for returning a Combine publisher.
The Remaining Combine Use Cases
Keep Combine for the specific scenarios where its operators save you real code: search debouncing with $searchText, timer-based updates, and complex multi-stream coordination. These are legitimate use cases that Combine handles well. Just keep them contained rather than spreading Combine throughout your architecture.
Migrating from Combine to Async/Await
If you have an existing codebase heavy on Combine, here is a practical migration strategy:
- Do not rewrite everything at once. Combine works. It is not broken. Migrate incrementally as you touch code for other reasons.
- Start with the service layer. Convert API clients from returning publishers to
asyncfunctions. This is the highest-impact, lowest-risk migration. - Replace ObservableObject with @Observable. This removes the need for
@Publishedand eliminates most Combine usage in ViewModels. It also simplifies your views because you drop@StateObjectand@ObservedObjectin favor of@State. - Use .values to bridge. Where you still have Combine publishers you are not ready to replace, use the
.valuesproperty to consume them as AsyncSequence in async contexts. This lets new code use async/await while old publishers still work. - Keep Combine for genuine reactive patterns. Do not force-migrate debounce pipelines or combineLatest chains if they are working well. Migrate them later when Swift Async Algorithms has matured further, or leave them as-is.
What About Swift Async Algorithms?
The Swift Async Algorithms package is Apple's answer to "what replaces Combine's operators in the async/await world?" It extends AsyncSequence with operators like debounce, throttle, combineLatest, and merge.
It is open-source, actively maintained, and the operator set is growing. However, it is not yet a complete replacement for Combine's full operator library. Operators like switchToLatest, backpressure handling, and some of Combine's more advanced composition patterns do not have direct equivalents yet.
The trajectory is clear: Swift Async Algorithms will eventually cover most of what Combine does. But "eventually" is not "today." For now, treat it as a supplement. Use it when the operator you need exists. Fall back to Combine when it does not.
The Bottom Line
Async/await and Combine are not competing tools. They solve overlapping but different problems, and the smartest approach is using both where each fits naturally.
For new SwiftUI projects in 2026: default to async/await and @Observable. Use .task for loading data. Use async functions in your services. Reach for Combine only when you need reactive operators (debounce, combineLatest, timers) that genuinely simplify your code. This approach gives you the cleanest, most maintainable codebase with the best forward compatibility as Swift Concurrency continues to evolve.
If you want to see both patterns applied in a production-ready architecture, our MVVM architecture guide shows how to structure ViewModels and services using modern Swift Concurrency with Combine where appropriate.