---
title: "Coroutines Avancadas: Structured Concurrency Kotlin"
url: "https://kotlin.dev.br/blog/coroutines-avancadas-structured-concurrency-kotlin/"
markdown_url: "https://kotlin.dev.br/blog/coroutines-avancadas-structured-concurrency-kotlin.MD"
description: "Domine structured concurrency em Kotlin: coroutineScope, supervisorScope, tratamento de excecoes, cancelamento e testes com runTest."
date: "2026-04-29"
author: "Karina Melo"
---

# Coroutines Avancadas: Structured Concurrency Kotlin

Domine structured concurrency em Kotlin: coroutineScope, supervisorScope, tratamento de excecoes, cancelamento e testes com runTest.


Se voce ja entende o basico de [coroutines em Kotlin](/blog/coroutines-kotlin/) — `launch`, `async`, `suspend` — e hora de dominar os padroes avancados que separam codigo de producao de codigo de tutorial. Structured concurrency e o principio fundamental que garante que coroutines nao vazem, excecoes nao sejam silenciadas e recursos sejam liberados de forma previsivel.

Neste artigo, vamos explorar na pratica: hierarquia de Jobs, `coroutineScope` vs `supervisorScope`, propagacao de excecoes, cancelamento cooperativo e como testar tudo isso com `runTest`.

## O principio da structured concurrency

Structured concurrency significa que toda coroutine tem um escopo bem definido e um pai. Quando o pai e cancelado, todos os filhos sao cancelados automaticamente. Quando um filho falha, o pai e notificado. Nao existem coroutines orfas.

Esse contrato e garantido pela hierarquia de `Job`. Cada `CoroutineScope` tem um `Job` pai, e cada `launch` ou `async` cria um `Job` filho:

```kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    val parentJob = coroutineContext[Job]
    println("Filhos antes: ${parentJob?.children?.count()}")

    launch {
        delay(1000)
        println("Coroutine 1 concluida")
    }

    launch {
        delay(500)
        println("Coroutine 2 concluida")
    }

    println("Filhos depois: ${parentJob?.children?.count()}")
}
```

Esse codigo imprime 2 filhos ativos. O `runBlocking` so termina quando ambos completam. Nenhuma coroutine escapa do escopo — esse e o contrato fundamental.

## coroutineScope vs supervisorScope

A diferenca entre esses dois builders define como falhas se propagam:

- **`coroutineScope`**: se qualquer filho falha, todos os outros filhos sao cancelados e a excecao e relancada.
- **`supervisorScope`**: se um filho falha, os outros continuam executando. A falha nao se propaga automaticamente.

Veja a diferenca na pratica:

```kotlin
import kotlinx.coroutines.*

suspend fun fetchDashboardData() = supervisorScope {
    val userDeferred = async { fetchUserProfile() }
    val notificationsDeferred = async { fetchNotifications() }
    val analyticsDeferred = async { fetchAnalytics() }

    val user = userDeferred.await()
    val notifications = try {
        notificationsDeferred.await()
    } catch (e: Exception) {
        println("Notificacoes indisponiveis: ${e.message}")
        emptyList()
    }
    val analytics = try {
        analyticsDeferred.await()
    } catch (e: Exception) {
        println("Analytics indisponivel: ${e.message}")
        null
    }

    DashboardData(user, notifications, analytics)
}
```

Com `supervisorScope`, se `fetchNotifications()` falhar, o perfil do usuario e os analytics continuam carregando normalmente. Se usassemos `coroutineScope`, a falha nas notificacoes cancelaria todas as outras chamadas — comportamento indesejado em um dashboard.

Esse padrao e especialmente util em APIs construidas com [Ktor](/blog/ktor-3-4-novidades-openapi-streaming-2026/), onde multiplas chamadas a servicos externos acontecem em paralelo.

## Tratamento de excecoes e CoroutineExceptionHandler

Excecoes em coroutines seguem regras diferentes de codigo sincrono. Um `try/catch` dentro de `launch` captura a excecao normalmente, mas nao impede que ela se propague pela hierarquia de Jobs.

Para capturar excecoes nao tratadas no nivel do escopo, use `CoroutineExceptionHandler`:

```kotlin
import kotlinx.coroutines.*

val handler = CoroutineExceptionHandler { context, exception ->
    println("Excecao capturada: ${exception.message}")
    // Enviar para sistema de observabilidade
}

fun main() = runBlocking {
    val scope = CoroutineScope(SupervisorJob() + handler + Dispatchers.Default)

    scope.launch {
        throw RuntimeException("Falha no processamento")
    }

    scope.launch {
        delay(100)
        println("Esta coroutine continua executando")
    }

    delay(200)
    scope.cancel()
}
```

O `CoroutineExceptionHandler` funciona como uma rede de seguranca. Ele so captura excecoes que nao foram tratadas por `try/catch` e so funciona com `launch` (nao com `async`, onde a excecao e armazenada no `Deferred`). Para [observabilidade em producao](/blog/kotlin-observabilidade/), integrar o handler com ferramentas de tracing e essencial.

## Cancelamento cooperativo

O cancelamento em coroutines e **cooperativo**. Isso significa que uma coroutine so e cancelada se ela verifica o status de cancelamento. Funcoes como `delay()`, `yield()` e as operacoes de I/O de `kotlinx.coroutines` fazem essa verificacao automaticamente.

Para loops de processamento intensivo, voce precisa verificar manualmente:

```kotlin
import kotlinx.coroutines.*

suspend fun processLargeDataset(items: List<DataItem>) = coroutineScope {
    items.forEachIndexed { index, item ->
        ensureActive() // Verifica cancelamento a cada iteracao
        processItem(item)

        if (index % 100 == 0) {
            yield() // Cede o thread para outras coroutines
        }
    }
}
```

Sem `ensureActive()` ou `yield()`, a coroutine ignora requisicoes de cancelamento e continua executando ate o fim — desperdicando recursos. Esse e um dos [padroes de design](/blog/kotlin-design-patterns/) mais importantes para codigo assincrono robusto.

## Trocando contexto com withContext

O `withContext` troca o dispatcher sem criar uma nova coroutine. E a forma idiomatica de mover trabalho para um thread diferente:

```kotlin
import kotlinx.coroutines.*

suspend fun loadAndParseFile(path: String): ParsedData {
    val rawBytes = withContext(Dispatchers.IO) {
        File(path).readBytes()
    }

    val parsed = withContext(Dispatchers.Default) {
        parseComplexFormat(rawBytes)
    }

    return parsed
}
```

`Dispatchers.IO` e otimizado para operacoes de I/O bloqueantes (leitura de arquivo, chamadas de rede). `Dispatchers.Default` usa um pool com tamanho igual ao numero de cores da CPU, ideal para processamento intensivo. Nunca faca I/O bloqueante no `Dispatchers.Main` ou `Dispatchers.Default`.

## Tratamento de erros em Flows

[Kotlin Flow](/blog/kotlin-flow/) herda os principios de structured concurrency, mas tem operadores especificos para tratamento de erros:

```kotlin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*

fun fetchPricesFlow(): Flow<Double> = flow {
    while (true) {
        val price = fetchCurrentPrice()
        emit(price)
        delay(5000)
    }
}.retry(retries = 3) { cause ->
    cause is java.io.IOException
}.catch { e ->
    println("Stream encerrado: ${e.message}")
    emit(-1.0) // Valor sentinela
}
```

O operador `retry` re-executa o bloco `flow` quando a excecao corresponde ao predicado. O `catch` captura qualquer excecao que passe pelo `retry` e permite emitir um valor final antes de encerrar o stream.

## Testando coroutines com runTest

O `kotlinx-coroutines-test` fornece `runTest`, que executa coroutines em tempo virtual — sem esperar `delay()` reais:

```kotlin
import kotlinx.coroutines.test.*
import kotlin.test.Test
import kotlin.test.assertEquals

class DashboardServiceTest {

    @Test
    fun `deve retornar dashboard mesmo com falha parcial`() = runTest {
        val service = DashboardService(
            userApi = FakeUserApi(),
            notificationApi = FailingNotificationApi(),
            analyticsApi = FakeAnalyticsApi()
        )

        val result = service.fetchDashboardData()

        assertEquals("Joao", result.user.name)
        assertEquals(emptyList(), result.notifications)
    }
}
```

O `runTest` avanca o tempo virtual automaticamente quando encontra `delay()`. Para cenarios onde voce precisa controlar o tempo manualmente, use `advanceTimeBy()` e `advanceUntilIdle()`. Para mais detalhes sobre [testes em Kotlin com JUnit5 e MockK](/blog/kotlin-testes-junit5-mockk-guia/), confira nosso guia dedicado.

## Boas praticas para producao

1. **Nunca use `GlobalScope`** — ele cria coroutines sem pai, quebrando structured concurrency.
2. **Prefira `supervisorScope` em pontos de entrada** como handlers HTTP ou processadores de filas.
3. **Sempre use `ensureActive()` em loops longos** para respeitar cancelamento.
4. **Injete dispatchers via parametro** para facilitar testes.
5. **Trate excecoes no nivel mais proximo possivel** antes de deixar o `CoroutineExceptionHandler` como ultima defesa.

Desenvolvedores que trabalham com concorrencia em outras linguagens encontram paralelos interessantes. Em <a href="https://golang.com.br/blog" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', {destination: 'golang.com.br'})">Go</a>, goroutines com `errgroup` oferecem structured concurrency similar ao `coroutineScope`, mas sem hierarquia automatica de cancelamento. Em <a href="https://python.dev.br/blog" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', {destination: 'python.dev.br'})">Python</a>, o `asyncio.TaskGroup` introduzido no Python 3.11 se inspirou diretamente nos mesmos principios de structured concurrency do Kotlin.

## Conclusao

Structured concurrency nao e apenas um conceito teorico — e o alicerce que torna coroutines seguras para producao. Dominar `coroutineScope`, `supervisorScope`, cancelamento cooperativo e `runTest` transforma a forma como voce escreve codigo assincrono em Kotlin. Aplique esses padroes no seu proximo projeto e veja a diferenca na robustez e testabilidade do codigo.

## Perguntas frequentes

### Qual a diferenca entre coroutineScope e supervisorScope?

O `coroutineScope` cancela todos os filhos quando um deles falha, e a excecao e relancada. O `supervisorScope` permite que os demais filhos continuem executando mesmo quando um falha, ideal para cenarios como dashboards e chamadas paralelas independentes.

### Por que nao devo usar GlobalScope?

O `GlobalScope` cria coroutines sem pai na hierarquia de Jobs, o que significa que elas nao sao canceladas automaticamente quando o escopo que as criou termina. Isso pode causar vazamento de coroutines e comportamento imprevisivel.

### Como testar funcoes suspend que usam delay?

Use `runTest` do pacote `kotlinx-coroutines-test`. Ele executa coroutines em tempo virtual, avancando automaticamente os delays sem esperar tempo real. Para controle fino, use `advanceTimeBy()` e `advanceUntilIdle()`.

### O CoroutineExceptionHandler substitui try/catch?

Nao. O `CoroutineExceptionHandler` e uma rede de seguranca para excecoes nao tratadas em coroutines lancadas com `launch`. Voce deve usar `try/catch` para tratamento local e especifico. O handler serve como ultima defesa para evitar crashes silenciosos.

Se você quer explorar modelos de concorrência em outras linguagens, <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go oferece goroutines com structured concurrency via errgroup</a>, <a href="https://rustlang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust usa async/await com Tokio e supervisão explícita</a> e <a href="https://ziglang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'ziglang.com.br' })">Zig oferece async com controle manual de alocação</a>.
