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