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 usa
panic!la macro para terminar el programa - Errores recuperables(Recoverable): la operación puede fallar pero puede manejarse; se usa
Result<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.
1. Errores irrecuperables: panic!
panic!La macro se usa para indicar que el programa encontró un error grave que le impide continuar. Al invocarla:
- Imprime el mensaje de error y la ubicación donde ocurrió
- Expande (unwind) la pila de llamadas y limpia los recursos
- Termina el programa
Ejemplo
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 usar
panic!: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<T, E> {
Ok(T), // 操作成功,包含结果值
Err(E), // 操作失败,包含错误信息
}
Todas las funciones de la biblioteca estándar de Rust que pueden fallar devuelvenResult. Por ejemplo, abrir un archivo:
2.1 Usar match para manejar Result
Ejemplo
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
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
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 usar
expecten 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::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::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<String, io::Error> {
let mut content = String::new();
File::open(path)?.read_to_string(&mut content)?;
Ok(content)
}
?Funcionamiento del operador:
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::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::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::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 usar
thiserrorla 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<T> {
Some(T), // 有值
None, // 没有值
}
Optionen otros lenguajesnull. Rust no tienenull, cualquier valor que pueda ser nulo debe usarOptionpara envolverse:
Ejemplo
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 |
Otras extensionesLa 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.