Tratamento de erros assíncronos no Rust
1. Fundamentos do tratamento de erros em Rust aplicados a código assíncrono
1.1. Revisão rápida: Result, Option e o operador ? em contextos síncronos
Em Rust, o tratamento de erros é feito principalmente através dos tipos Result<T, E> e Option<T>. O operador ? simplifica a propagação de erros: se o valor for Ok ou Some, ele é desempacotado; caso contrário, o erro é retornado imediatamente da função.
use std::fs::File;
use std::io::{self, Read};
fn ler_arquivo(caminho: &str) -> io::Result<String> {
let mut arquivo = File::open(caminho)?;
let mut conteudo = String::new();
arquivo.read_to_string(&mut conteudo)?;
Ok(conteudo)
}
1.2. Desafios específicos do assíncrono: propagação de erros através de Futures
Em código assíncrono, Futures representam computações que ainda não foram concluídas. Erros precisam ser propagados através desses Futures, e o operador ? só funciona dentro de funções que retornam Result. Em closures assíncronas, isso exige cuidado extra.
use tokio::fs;
async fn ler_arquivo_assincrono(caminho: &str) -> io::Result<String> {
let mut arquivo = fs::File::open(caminho).await?;
let mut conteudo = String::new();
arquivo.read_to_string(&mut conteudo).await?;
Ok(conteudo)
}
1.3. A diferença entre erros em closures síncronas e assíncronas
Em closures síncronas, map e and_then funcionam naturalmente. Em closures assíncronas, precisamos de versões como then do StreamExt ou FutureExt.
use futures::future::FutureExt;
// Síncrono: funciona diretamente
let resultado: Result<i32, &str> = Ok(10);
let dobrado = resultado.map(|x| x * 2);
// Assíncrono: precisa de tratamento especial
async fn processar(valor: i32) -> Result<i32, &str> {
Ok(valor * 2)
}
let futuro = async { Ok::<_, &str>(10) };
let tratado = futuro.map(|res| res.map(|x| x * 3));
2. Propagação de erros com ? em funções assíncronas e streams
2.1. Uso do operador ? dentro de funções async fn
O operador ? funciona perfeitamente em funções async fn que retornam Result<T, E>.
use tokio::fs;
use std::io;
async fn copiar_arquivo(origem: &str, destino: &str) -> io::Result<()> {
let conteudo = fs::read_to_string(origem).await?;
fs::write(destino, &conteudo).await?;
Ok(())
}
2.2. Propagação de erros em streams assíncronos
Streams assíncronos oferecem métodos como try_for_each, try_collect e try_fold para lidar com erros de forma elegante.
use tokio_stream::StreamExt;
use tokio::fs;
async fn processar_arquivos(caminhos: Vec<&str>) -> io::Result<Vec<String>> {
let stream = tokio_stream::iter(caminhos);
stream
.then(|caminho| fs::read_to_string(caminho))
.try_collect::<Vec<_>>()
.await
}
2.3. Limitações do ? em closures e como contornar
Em closures passadas para then ou map, o ? não funciona diretamente. Use try_join! para combinar múltiplos futuros que podem falhar.
use tokio::try_join;
use tokio::fs;
async fn ler_multiplos(a: &str, b: &str) -> io::Result<(String, String)> {
let futuro_a = fs::read_to_string(a);
let futuro_b = fs::read_to_string(b);
let (conteudo_a, conteudo_b) = try_join!(futuro_a, futuro_b)?;
Ok((conteudo_a, conteudo_b))
}
3. Tratamento de erros em tarefas Tokio: JoinHandle e JoinError
3.1. Capturando erros de tarefas com JoinHandle::await
Quando você spawna uma tarefa com tokio::spawn, recebe um JoinHandle. Ao fazer await, você pode obter um JoinError.
use tokio::task;
#[tokio::main]
async fn main() {
let handle: task::JoinHandle<i32> = tokio::spawn(async {
// Simulando um erro
panic!("algo deu errado");
});
match handle.await {
Ok(valor) => println!("Sucesso: {}", valor),
Err(join_error) => {
if join_error.is_panic() {
println!("A tarefa entrou em pânico!");
// Recupera a mensagem de pânico
let _ = join_error.into_panic();
} else {
println!("Tarefa cancelada");
}
}
}
}
3.2. Diferença entre pânico e erro retornado
Um JoinError pode representar tanto um pânico (panic!) quanto um cancelamento da tarefa. Erros retornados via Result são capturados como Ok(Err(...)).
use tokio::task;
#[tokio::main]
async fn main() {
// Erro retornado via Result
let handle = tokio::spawn(async {
Err::<i32, &str>("erro planejado")
});
match handle.await {
Ok(Ok(valor)) => println!("Sucesso: {}", valor),
Ok(Err(erro)) => println!("Erro retornado: {}", erro),
Err(join_error) => println!("Pânico ou cancelamento: {:?}", join_error),
}
}
3.3. Estratégias para lidar com JoinError
Use is_panic() para verificar se houve pânico e try_into_panic() para recuperar a mensagem.
use tokio::task;
fn lidar_com_join_error(erro: task::JoinError) {
if erro.is_panic() {
eprintln!("Recuperando de pânico...");
// Estratégia: re-executar a tarefa
// ou logar o erro e continuar
} else {
eprintln!("Tarefa cancelada, ignorando");
}
}
4. Erros em canais assíncronos (Tokio mpsc, oneshot, broadcast)
4.1. Erros de envio: canal fechado
Canais mpsc retornam SendError quando o receptor foi dropado.
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<i32>(10);
drop(rx); // Fecha o canal
match tx.send(42).await {
Ok(_) => println!("Enviado com sucesso"),
Err(send_error) => {
println!("Falha ao enviar: receptor fechado");
// send_error.0 contém o valor não enviado
let _valor_perdido = send_error.0;
}
}
}
4.2. Erros de recebimento
RecvError indica que o canal foi fechado e não há mais mensagens.
use tokio::sync::oneshot;
#[tokio::main]
async fn main() {
let (tx, rx) = oneshot::channel::<i32>();
drop(tx); // Fecha o canal sem enviar
match rx.await {
Ok(valor) => println!("Recebido: {}", valor),
Err(RecvError) => println!("Canal fechado sem mensagem"),
}
}
4.3. Erros em broadcast
No broadcast, RecvError::Lagged ocorre quando o receptor perde mensagens por estar atrasado.
use tokio::sync::broadcast;
#[tokio::main]
async fn main() {
let (tx, mut rx) = broadcast::channel::<i32>(2);
tx.send(1).unwrap();
tx.send(2).unwrap();
tx.send(3).unwrap(); // Receptor perdeu a mensagem 1
match rx.recv().await {
Ok(msg) => println!("Recebido: {}", msg),
Err(broadcast::error::RecvError::Lagged(n)) => {
println!("Perdeu {} mensagens", n);
}
Err(broadcast::error::RecvError::Closed) => {
println!("Canal fechado");
}
}
}
5. Combinadores de erro para Future e Stream
5.1. Uso de FutureExt::map e Result::into_future
Transforme erros de forma elegante com combinadores.
use futures::future::FutureExt;
async fn operacao_falivel() -> Result<i32, &'static str> {
Ok(42)
}
#[tokio::main]
async fn main() {
let futuro = operacao_falivel();
let tratado = futuro.map(|res| res.map_err(|e| format!("Erro: {}", e)));
let resultado: Result<i32, String> = tratado.await;
println!("{:?}", resultado);
}
5.2. Combinadores de Stream
StreamExt::or_else permite fornecer fallback para erros.
use tokio_stream::StreamExt;
use futures::stream;
async fn processar_item(item: i32) -> Result<i32, &'static str> {
if item < 0 {
Err("valor negativo")
} else {
Ok(item * 2)
}
}
#[tokio::main]
async fn main() {
let stream = stream::iter(vec![1, -2, 3]);
let resultado: Vec<_> = stream
.then(processar_item)
.or_else(|erro| async move {
eprintln!("Erro: {}", erro);
Ok::<_, ()>(0) // fallback
})
.collect()
.await;
println!("{:?}", resultado);
}
5.3. Tratamento de erros em select! e join!
Use select! com fallback parcial.
use tokio::select;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let resultado = select! {
resultado = operacao_rapida() => resultado,
_ = sleep(Duration::from_secs(1)) => Err("timeout"),
};
match resultado {
Ok(v) => println!("Sucesso: {}", v),
Err(e) => println!("Erro: {}", e),
}
}
async fn operacao_rapida() -> Result<i32, &'static str> {
sleep(Duration::from_millis(500)).await;
Ok(10)
}
6. Erros em operações de I/O assíncronas e tempo
6.1. Erros de tokio::io
Operações de I/O retornam io::Error, tratado com ?.
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
async fn ler_dados(stream: &mut TcpStream) -> std::io::Result<Vec<u8>> {
let mut buffer = vec![0u8; 1024];
let n = stream.read(&mut buffer).await?;
buffer.truncate(n);
Ok(buffer)
}
6.2. Timeouts
tokio::time::timeout retorna Elapsed como erro.
use tokio::time::{timeout, Duration};
async fn operacao_lenta() -> Result<i32, &'static str> {
tokio::time::sleep(Duration::from_secs(5)).await;
Ok(42)
}
#[tokio::main]
async fn main() {
match timeout(Duration::from_secs(1), operacao_lenta()).await {
Ok(Ok(v)) => println!("Sucesso: {}", v),
Ok(Err(e)) => println!("Erro da operação: {}", e),
Err(_elapsed) => println!("Timeout!"),
}
}
6.3. Erros em tokio::fs e tokio::net
Reutilize padrões síncronos com adaptação assíncrona.
use tokio::fs;
async fn ler_arquivo_seguro(caminho: &str) -> Result<String, Box<dyn std::error::Error>> {
let conteudo = fs::read_to_string(caminho).await?;
Ok(conteudo)
}
7. Boas práticas e padrões avançados
7.1. Tipos de erro personalizados com thiserror e anyhow
Use thiserror para erros em bibliotecas e anyhow para aplicações.
use thiserror::Error;
use anyhow::Result;
#[derive(Error, Debug)]
enum MeuErro {
#[error("erro de I/O: {0}")]
Io(#[from] std::io::Error),
#[error("timeout após {0} segundos")]
Timeout(u64),
}
async fn operacao() -> Result<(), MeuErro> {
// Uso com anyhow para simplificar
Ok(())
}
7.2. Uso de yield_now para evitar estouro de pilha
Em loops longos com tratamento de erro, ceda o controle para evitar estouro.
use tokio::task;
async fn processar_lote(itens: Vec<i32>) -> Result<(), &'static str> {
for item in itens {
if item < 0 {
return Err("item negativo");
}
task::yield_now().await; // Evita estouro de pilha
}
Ok(())
}
7.3. Testes de erro em código assíncrono
Use tokio::test e assert_matches! para testar erros.
#[cfg(test)]
mod tests {
use tokio::test;
use assert_matches::assert_matches;
#[test]
async fn test_erro_timeout() {
let resultado = timeout(Duration::from_millis(1), async {
tokio::time::sleep(Duration::from_secs(1)).await;
}).await;
assert_matches!(resultado, Err(_));
}
}
Referências
- Documentação oficial do Tokio: Tratamento de erros — Documentação completa dos tipos de erro do Tokio, incluindo JoinError, SendError e RecvError.
- The Rust Async Book: Error Handling — Capítulo oficial sobre tratamento de erros em código assíncrono no Rust.
- Futures-rs crate: StreamExt — Documentação dos combinadores de stream como try_for_each, or_else e then.
- Tokio Tutorial: Channels — Tutorial oficial do Tokio sobre canais assíncronos e tratamento de erros de comunicação.
- Thiserror crate documentation — Como criar tipos de erro personalizados de forma ergonômica para uso em código assíncrono.