Se voce ja entende o basico de coroutines em 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:
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:
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, 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:
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, 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:
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 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:
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 herda os principios de structured concurrency, mas tem operadores especificos para tratamento de erros:
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:
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, confira nosso guia dedicado.
Boas praticas para producao
- Nunca use
GlobalScope— ele cria coroutines sem pai, quebrando structured concurrency. - Prefira
supervisorScopeem pontos de entrada como handlers HTTP ou processadores de filas. - Sempre use
ensureActive()em loops longos para respeitar cancelamento. - Injete dispatchers via parametro para facilitar testes.
- Trate excecoes no nivel mais proximo possivel antes de deixar o
CoroutineExceptionHandlercomo ultima defesa.
Desenvolvedores que trabalham com concorrencia em outras linguagens encontram paralelos interessantes. Em Go, goroutines com errgroup oferecem structured concurrency similar ao coroutineScope, mas sem hierarquia automatica de cancelamento. Em Python, 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, Go oferece goroutines com structured concurrency via errgroup, Rust usa async/await com Tokio e supervisão explícita e Zig oferece async com controle manual de alocação.