Structured concurrency means every coroutine has a defined lifetime, tied to a parent scope — no orphaned background work that outlives what launched it. You already used this with viewModelScope; here’s the underlying mechanics:

suspend fun loadDashboard() = coroutineScope {
    val userDeferred = async { fetchUser() }
    val postsDeferred = async { fetchPosts() }

    // Both run concurrently; this suspends until BOTH complete
    val user = userDeferred.await()
    val posts = postsDeferred.await()
    Dashboard(user, posts)
}

If fetchPosts() throws, coroutineScope automatically cancels the sibling userDeferred coroutine too — structured concurrency propagates failure and cancellation through the whole tree, preventing half-finished work from silently continuing.

Handling exceptions properly

viewModelScope.launch {
    try {
        val result = repository.riskyOperation()
        _state.value = UiState.Success(result)
    } catch (e: CancellationException) {
        throw e   // NEVER swallow cancellation — rethrow it always
    } catch (e: Exception) {
        _state.value = UiState.Error(e.message ?: "Unknown error")
    }
}

Swallowing CancellationException is a classic, hard-to-debug mistake — it breaks structured concurrency’s ability to cancel a coroutine tree cleanly.