Kotlin Coroutines vs Dart's async/await: A Practical Comparison
Same Problem, Different Concurrency Models
If you split your time between a Kotlin/KMM codebase and a Flutter one, it's easy to assume suspend fun and async are basically the same thing wearing different syntax. They're not — the underlying concurrency models are meaningfully different.
Syntax comparison
// Kotlin
suspend fun fetchUser(id: String): User {
val response = httpClient.get("/users/$id")
return response.body()
}
// Dart
Future fetchUser(String id) async {
final response = await httpClient.get('/users/$id');
return User.fromJson(response.body);
}Nearly identical to read. The difference is what's happening underneath.
Dart: single-threaded, event-loop concurrency
Dart is fundamentally single-threaded per isolate. async/await doesn't create parallelism — it schedules continuations on the same event loop, similar to JavaScript's Promise model. True parallel CPU work requires spinning up a separate Isolate (Dart's actor-like unit with its own memory heap), which is heavier-weight and requires explicit message passing.
Kotlin: structured concurrency on real threads
Kotlin coroutines are lightweight, but they run on real thread pools via Dispatchers (Dispatchers.IO, Dispatchers.Default, Dispatchers.Main). suspend functions can genuinely run in parallel across CPU cores without any equivalent of spawning an isolate:
suspend fun loadDashboard() = coroutineScope {
val user = async(Dispatchers.IO) { fetchUser() }
val stats = async(Dispatchers.IO) { fetchStats() }
Dashboard(user.await(), stats.await())
}This is genuine parallel I/O and CPU work, not just interleaved single-thread scheduling. Structured concurrency (coroutineScope, viewModelScope) also guarantees child coroutines are cancelled when the parent scope is, which Dart's Futures don't give you automatically — cancelling a Dart Future generally requires manual bookkeeping (a CancelToken pattern) since Futures aren't cancellable by design.
Practical implications
| Kotlin Coroutines | Dart async/await | |
|---|---|---|
| True parallelism | Yes, via dispatchers/threads | Only via Isolates (heavyweight) |
| Cancellation | Built-in, structured | Manual (no native Future cancellation) |
| Error propagation | Structured, scoped exception handling | Standard try/catch per Future chain |
| Cost of a "lightweight" unit | Very cheap (thousands of coroutines fine) | Futures are cheap; Isolates are not |
The takeaway
Don't port Kotlin concurrency habits directly into Dart. If you're doing genuinely CPU-heavy work in Flutter (image processing, heavy JSON parsing), reach for compute() or an Isolate — an async function alone won't get you off the main isolate. And when working in Kotlin, take advantage of structured concurrency and cancellation — it's a real safety net Dart doesn't hand you for free.