Short answer
launch starts a coroutine in a fire-and-forget style and returns a Job object; async starts a task that returns an expected result as a Deferred<T> retrieved through await().
Explanation
launch is used when we do not need a value returned from the coroutine (such as logging an event or saving a record). async is used when you want to compute a value, especially to run several concurrent requests in the background and wait until all of them complete through await() or awaitAll(). Note that exception behavior depends on where the async is: if it is a child inside a regular scope (such as coroutineScope or viewModelScope) its failure cancels the parent and siblings immediately and the error propagates even if await() is never called; a root async (such as GlobalScope.async or inside supervisorScope) keeps the exception inside the Deferred and it only appears at await().
Code example
// Run two network requests concurrently to reduce wait time:
suspend fun fetchDashboardData(): Dashboard = coroutineScope {
val profileDeferred = async { api.getProfile() }
val statsDeferred = async { api.getStats() }
// Wait for both to complete in parallel:
Dashboard(profileDeferred.await(), statsDeferred.await())
}Common mistakes
- Using async instead of launch without calling await(); in a root scope or inside supervisorScope the error is kept inside the Deferred and never appears, so you never realize a problem happened.
- Calling await() immediately after async on the very next line, which cancels the benefit of parallelism and makes the code sequential and slow.