skip to content

When should you choose launch over async, and what problems arise from misusing launch for fire-and-forget work?

level: seniorimportance: should knowfreq 45%

answer

  1. launch = effects/no result; async = value via await
  2. Avoid GlobalScope — no lifecycle, leaks
  3. Keep the Job if you need cancel/join
  4. Never swallow CancellationException
  5. launch failures propagate, not silently dropped

basics

~20 s

Use launch when you start work but don't need its result; use async when you need a value back. Misusing launch — like running it on GlobalScope — can leak coroutines, hide errors, or outlive the component that started it.

solid answer

~50 s

Choose launch for side-effecting, fire-and-forget work whose result you don't consume: writing to a DB, emitting an event, updating UI state, starting a collector. Choose async only when you actually need the produced value via await(). The classic anti-patterns with launch: (1) launching on GlobalScope (or a long-lived app scope) for short-lived work, which detaches the coroutine from any lifecycle so it can leak or outlive its component; (2) using launch when you need the result and then trying to smuggle it out through shared mutable state instead of async/await; (3) ignoring the returned Job so you can't cancel or join it; (4) swallowing CancellationException in a catch block, breaking cooperative cancellation; (5) assuming launch failures are silently dropped — they propagate to the parent and can crash the scope. The right default is to launch within a structured, lifecycle-bound scope and let cancellation flow.

go deeper

for a junior

Picks launch for no-result work and async for a value.

for a middle

Explains GlobalScope leak risk and keeping the Job for cancellation.

for a senior

Reasons about lifecycle-bound scopes, cancellation cooperation, and unawaited-async pitfalls together.

for a principal

Sets team conventions for scope ownership, structured concurrency, and error/cancellation handling to avoid leaks across the codebase.

## The decision rule - **`launch`** → you want to *start* concurrent work and you do **not** consume a return value in the caller. It returns a `Job`. Use for side effects: persistence, logging/analytics, firing an event, starting a `Flow` collector, updating observable state. - **`async`** → you want a **value** back, obtained with `await()`. It returns `Deferred<T>`. If the only reason you'd reach for `async` is 'to start it concurrently', and you never call `await()`, prefer `launch` — an `async` whose result is never awaited can swallow exceptions until/unless awaited. ## Anti-patterns when using `launch` ### 1. Launching on `GlobalScope` ```kotlin // Anti-pattern: detached from any lifecycle GlobalScope.launch { repository.sync() } ``` `GlobalScope` has **no lifetime owner**. The coroutine isn't a child of any structured scope, so nothing cancels it when the screen/request/component goes away. This leaks work, can touch destroyed UI, and breaks structured concurrency. Prefer a lifecycle-bound scope (`viewModelScope`, `lifecycleScope`, a request-scoped `CoroutineScope`, or `coroutineScope { }`). ### 2. Using `launch` when you actually need the result Reaching for shared `var` state to pass a result out of a `launch` block is a race-prone smell. If you need the value, use `async`/`await`. ### 3. Dropping the `Job` Ignoring the returned `Job` means you can't `cancel()`, `join()`, or observe completion. Keep it if you need lifecycle control. ### 4. Swallowing `CancellationException` ```kotlin launch { try { doWork() } catch (e: Exception) { log(e) } // BUG: also eats CancellationException } ``` Catching `Exception` swallows `CancellationException`, defeating cooperative cancellation. **Re-throw** `CancellationException` (or catch the specific exceptions you expect). ### 5. Assuming failures vanish A `launch` failure is **not** silently dropped — it propagates to the parent and, under a regular `Job`, cancels siblings and can crash the scope. Don't rely on 'fire and forget = errors disappear'. ## Putting it together ```kotlin class MyViewModel(private val repo: Repo) : ViewModel() { fun refresh() { // side effect, no result needed -> launch, lifecycle-bound viewModelScope.launch { repo.sync() // cancelled automatically when ViewModel clears } } } ``` The rule of thumb: **`launch` for effects within a structured, lifecycle-owned scope; `async` only when a value must come back.**

  • Why is GlobalScope.launch discouraged for short-lived work?
    GlobalScope has no lifetime owner, so the coroutine isn't tied to any component's lifecycle; nothing cancels it when the component dies, risking leaks and work on destroyed state.
  • You used async but never call await(). What's the risk?
    An unawaited async can hold its exception unsurfaced (and the result is wasted). If you don't need the value, use launch so failures propagate normally.

launch is mailing a postcard (you don't expect a reply); async is sending a letter with a stamped return envelope (you wait for the answer).

saying these in an interview costs you the question

  • Defaulting to GlobalScope.launch instead of a lifecycle scope
  • Using launch + shared mutable var to return a result
  • Catch (e: Exception) that swallows CancellationException
  • Claiming launch errors are silently ignored
  • Using async purely to start work concurrently and never awaiting

context