Manejo de errores en Rust

Rust adopta un mecanismo único de manejo de errores,sin excepciones, sin try/catch.Divide los errores en dos categorías y los maneja de maneras diferentes:

  • Errores irrecuperables(Unrecoverable): problemas graves en la lógica del programa; se usapanic!la macro para terminar el programa
  • Errores recuperables(Recoverable): la operación puede fallar pero puede manejarse; se usaResult<T, E>la enumeración

Este diseño obliga a los desarrolladores a manejar los posibles errores en tiempo de compilación, en lugar de descubrir omisiones en tiempo de ejecución.

Panorama del mecanismo de manejo de errores en Rust Errores en el programa Errores irrecuperables panic!("error grave") Imprimir mensaje de error → expandir pila de llamadas → terminar el programa Ejemplos: índice fuera de rango, división por cero, bug de lógica → similar a una excepción no capturada en otros lenguajes Errores recuperables Result<T, E> Ok(T) indica éxito, Err(E) indica fallo Ejemplos: archivo inexistente, timeout de red, fallo de parsing → similar a las checked exceptions de otros lenguajes

1. Errores irrecuperables: panic!

panic!La macro se usa para indicar que el programa encontró un error grave que le impide continuar. Al invocarla:

  1. Imprime el mensaje de error y la ubicación donde ocurrió
  2. Expande (unwind) la pila de llamadas y limpia los recursos
  3. Termina el programa

Ejemplo

fn main() {
    panic!(Ocurrió un error grave);
    // El siguiente código nunca se ejecutará
    println!("Hello, Rust");
}

Resultado de ejecución:

thread 'main' panicked at '发生了严重错误', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

La salida muestra dos líneas de información:

  • Primera línea: la ubicación del panic (nombre de archivo y número de línea) y el mensaje de error
  • Segunda línea: sugiere cómo ver el backtrace completo de la pila de llamadas

Ver el backtrace de la pila de llamadas (Backtrace)

ConfiguraRUST_BACKTRACE=1la variable de entorno para ver la pila de llamadas completa y ayudar a localizar el origen del panic:

# Linux / macOS
RUST_BACKTRACE=1 cargo run

# Windows PowerShell
$env:RUST_BACKTRACE=1; cargo run

La salida del backtrace enumerará desde la ubicación del panic hastamainla cadena de llamadas completa de la función. Esta información es muy útil cuando el panic ocurre en llamadas profundas.

Cuándo usarpanic!:Cuando aparece un error lógico (bug) que no debería ocurrir, como acceso fuera de límites, desreferenciar un puntero nulo o violar un contrato inmutable. Para errores previsibles causados por entradas externas, se debe usarResult。


2. Errores recuperables: Result<T, E>

Resultes la enumeración de la biblioteca estándar de Rust que representa operaciones que pueden fallar:

enum Result&lt;T, E&gt; {
    Ok(T),    // 操作成功,包含结果值
    Err(E),   // 操作失败,包含错误信息
}

Todas las funciones de la biblioteca estándar de Rust que pueden fallar devuelvenResult. Por ejemplo, abrir un archivo:

File::open() devuelve Result<File, io::Error> let f = File::open("hello.txt"); El archivo existe Ok(file) Se obtiene el objeto File, se puede leer y escribir El archivo no existe Err(io::Error) Se obtiene un mensaje de error, se puede manejar

2.1 Usar match para manejar Result

Ejemplo

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    match f {
        Ok(file) => {
            println!(Archivo abierto con éxito: {:?}, file);
        }
        Err(error) => {
            println!(Error al abrir el archivo: {}, error);
        }
    }
}

2.2 Usar if let para simplificar el manejo

Cuando solo te importa el caso de éxito,if letCompararmatches más conciso:

Ejemplo

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    if let Ok(file) = f {
        println!(Archivo abierto con éxito);
        // Usa `file` aquí ...
    } else {
        println!(Error al abrir el archivo);
    }
}

2.3 unwrap y expect: rápidos pero peligrosos

Si estás seguro de que la operación no fallará (o en la fase de prototipo no quieres manejar errores), puedes usar estos dos métodos rápidos:

Método Comportamiento Mensaje de panic en caso de fallo
.unwrap() En caso de éxito devuelveT, en caso de fallo directamentepanic! usa el mensaje de error predeterminado
.expect("msg") En caso de éxito devuelveT, en caso de fallo directamentepanic! usa un mensaje de error personalizado (más fácil de depurar)

Ejemplo

use std::fs::File;

fn main() {
    // unwrap: panic en caso de fallo, usa el mensaje predeterminado
    // thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ...'
    let f1 = File::open("hello.txt").unwrap();

    // expect: panic en caso de fallo, usa mensaje personalizado (recomendado)
    // thread 'main' panicked at 'No se pudo abrir el archivo de configuración: ...'
    let f2 = File::open("hello.txt").expect(No se pudo abrir el archivo de configuración);
}

Consejo:En código de producción se prefiere usarexpecten lugar deunwrap, porque un mensaje de error personalizado te permite localizar rápidamente el problema durante un panic. Una mejor práctica es usar?el operador para propagar el error al llamador.


3. Propagación de errores: el operador ?

En el desarrollo real, cuando una función encuentra un error, a menudo no quiere manejarlo ella misma, sino quepropagael error al llamador. Rust proporciona?el operador ? para simplificar esta operación.

3.1 Propagación manual vs. operador ?

Primero veamos la forma de propagación manual: verbosa pero clara:

Ejemplo

use std::fs::File;
use std::io::{self, Read};

// Propagar errores manualmente (tedioso)
fn read_file_manual(path: &str) -> Result<String, io::Error> {
    let f = File::open(path);

    // Si falla la apertura, devuelve Err
    let mut file = match f {
        Ok(file) => file,
        Err(e) => return Err(e),  // Retorna el error anticipadamente
    };

    let mut content = String::new();

    // Si falla la lectura, devuelve Err
    match file.read_to_string(&mut content) {
        Ok(_) => Ok(content),
        Err(e) => Err(e),
    }
}

Usando?el operador, la misma lógica se puede simplificar a:

Ejemplo

use std::fs::File;
use std::io::{self, Read};

// Usando el operador ? (conciso)
fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;   // En caso de fallo, devuelve Err automáticamente
    let mut content = String::new();
    file.read_to_string(&mut content)?; // En caso de fallo, devuelve Err automáticamente
    Ok(content)
}

También se puede encadenar para simplificar aún más:

fn read_file(path: &str) -> Result&lt;String, io::Error&gt; {
    let mut content = String::new();
    File::open(path)?.read_to_string(&mut content)?;
    Ok(content)
}

?Funcionamiento del operador:

Flujo de trabajo del operador ? let val = some_operation()?; ¿Ok o Err? Result Ok val = el valor dentro de Ok Continuar ejecutando el código posterior Err return Err(e) retorna anticipadamente

Limitación importante:?El operador solo se puede usar en funciones que devuelvenResult(oOption). Desde Rust 1.39,mainla función `main` también puede devolverResult。

3.2 Usar ? en main

Por defecto, la funciónmaindevuelve()y no se puede usar?. Pero se puede hacer quemaindevuelvaResult:

Ejemplo

use std::fs::File;
use std::io::{self, Read};

fn read_file(path: &str) -> Result<String, io::Error> {
    let mut content = String::new();
    File::open(path)?.read_to_string(&mut content)?;
    Ok(content)
}

// main devuelve Result, así se puede usar ? en main
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let content = read_file("hello.txt")?;  // ? también se puede usar en main
    println!("{}", content);
    Ok(())
}

4. Tipos de error personalizados y manejo por categorías

En proyectos reales, normalmente necesitas manejar los errores de forma diferente según su tipo. Rust, mediante elkind()método `kind()`, logra esto:

Ejemplo

use std::fs::File;
use std::io::{self, Read};

// Envuelve la lectura del archivo como una función independiente, propaga errores con ?
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

fn main() {
    match read_text_from_file("hello.txt") {
        Ok(content) => println!(Contenido del archivo:\n{}", content),
        Err(e) => {
            // Manejar de forma diferente según el tipo de error
            match e.kind() {
                io::ErrorKind::NotFound => {
                    println!(El archivo no existe, verifique la ruta);
                }
                io::ErrorKind::PermissionDenied => {
                    println!(No tiene permiso para leer el archivo);
                }
                _ => {
                    println!(Error al leer el archivo: {}, e);
                }
            }
        }
    }
}

Resultado de ejecución (cuando el archivo no existe):

El archivo no existe, por favor verifique la ruta.

io::ErrorKindVariantes de uso común:

ErrorKind Significado
NotFound El archivo o directorio no existe
PermissionDenied Permisos insuficientes
AlreadyExists El archivo ya existe (al crearlo)
ConnectionRefused Conexión rechazada
TimedOut Tiempo de espera agotado
InvalidInput Parámetro inválido

5. Tipos de error personalizados

En los proyectos, normalmente necesitas definir tus propios tipos de error para representar errores en la lógica de negocio:

Ejemplo

use std::fmt;
use std::num::ParseIntError;

// Definir un enum de error personalizado
#[derive(Debug)]
enum AppError {
    IoError(std::io::Error),
    ParseError(ParseIntError),
    CustomError(String),
}

// Implementar el trait Display, para la salida formateada
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::IoError(e) => write!(f, "Error de IO: {}", e),
            AppError::ParseError(e) => write!(f, "Error de análisis: {}", e),
            AppError::CustomError(msg) => write!(f, "Error de negocio: {}", msg),
        }
    }
}

// Implementar el trait From, para que el operador ? convierta automáticamente los tipos de error
impl From<std::io::Error> for AppError {
    fn from(error: std::io::Error) -> Self {
        AppError::IoError(error)
    }
}

impl From<ParseIntError> for AppError {
    fn from(error: ParseIntError) -> Self {
        AppError::ParseError(error)
    }
}

// Ahora se pueden manejar diferentes tipos de error con ? en la misma función
fn process_config(path: &str) -> Result<i32, AppError> {
    let content = std::fs::read_to_string(path)?; // io::Error → AppError
    let value: i32 = content.trim().parse()?;      // ParseIntError → AppError

    if value < 0 {
        return Err(AppError::CustomError("El valor de configuración no puede ser negativo".into()));
    }

    Ok(value)
}

fn main() {
    match process_config("config.txt") {
        Ok(val) => println!("Valor de configuración: {}", val),
        Err(e) => println!("Error: {}", e),
    }
}

Recomendaciones de bibliotecas de terceros:En proyectos reales, se puede usarthiserrorla biblioteca para derivar automáticamenteDisplayyFromimplementaciones, reduciendo en gran medida el código repetitivo. Para código de capa de aplicación, laanyhowbiblioteca proporciona convenientesanyhow::Resulttipos, adecuados para desarrollo rápido.


6. Option<T>: el valor puede no existir

Además deResult, Rust tiene otro enum importante para manejar situaciones donde "puede no haber valor"—se usa para reemplazarOption<T>:

enum Option&lt;T&gt; {
    Some(T),  // 有值
    None,     // 没有值
}

Optionen otros lenguajesnull. Rust no tienenull, cualquier valor que pueda ser nulo debe usarOptionpara envolverse:

Ejemplo

fn find_user(id: u32) -> Option<String> {
    match id {
        1 => Some("Alice".to_string()),
        2 => Some("Bob".to_string()),
        _ => None, // El usuario no existe
    }
}

fn main() {
    // Usar match para manejar Option
    match find_user(1) {
        Some(name) => println!("Usuario encontrado: {}", name),
        None => println!("El usuario no existe"),
    }

    // Usar if let para simplificar
    if let Some(name) = find_user(99) {
        println!("Usuario encontrado: {}", name);
    } else {
        println!("El usuario no existe");
    }

    // unwrap_or proporciona un valor predeterminado
    let name = find_user(99).unwrap_or("Usuario anónimo".to_string());
    println!("Nombre de usuario: {}", name);  // Usuario anónimo

    // El operador ? también aplica a Option
    let first_char = get_first_char("hello");
    println!("Primera letra: {:?}", first_char);  // Some('h')
}

fn get_first_char(s: &str) -> Option<char> {
    s.chars().next() // Devuelve Option<char>
}

OptionyResultComparación:

Elemento de comparación Option<T> Result<T, E>
Uso El valor puede existir o no La operación puede tener éxito o fallar
Éxito Some(T) Ok(T)
Falla None(sin información adicional) Err(E)(incluye la causa del error)
Escenarios típicos Búsqueda, campos opcionales, valores predeterminados Operaciones de archivo, solicitudes de red, análisis
Conversión ok_or(err) → Result ok() → Option

Resumen

Escenario Método recomendado Descripción
El programa encuentra un bug irreparable panic!("原因") Termina el programa, para situaciones que no deberían ocurrir
La operación puede fallar DevolverResult<T, E> Obliga a quien llama a manejar el error
Propagar errores dentro de la función ?operador Devuelve Err automáticamente al fallar, extrae el valor al tener éxito
Prototipado rápido / pruebas .expect("原因") Panic al fallar, pero con un mensaje de error claro
El valor puede no existir Option<T> usarSome / NoneReemplaza a null
Manejar por separado según el tipo de error e.kind() Coincidir con la variante de error específica
Tipos de error personalizados ImplementarDisplay + From Junto con?implementar conversión automática

La filosofía de manejo de errores de Rust:Los errores son parte del sistema de tipos, no excepciones del flujo de control. El compilador te obliga a manejar cada caso de error posible, lo que hace que tu programa sea más confiable en tiempo de ejecución.

Otras extensiones