En esta página
Manejo de errores: Result, Option y el operador ?
Manejo de errores en Rust
Rust no tiene excepciones. En cambio, usa tipos del sistema de tipos para representar operaciones que pueden fallar: Result<T, E> y Option<T>. Este enfoque obliga al programador a considerar los casos de error explícitamente, eliminando toda una clase de bugs.
Recordatorio: Result y Option
// En la stdlib:
// enum Result<T, E> { Ok(T), Err(E) }
// enum Option<T> { Some(T), None }
fn main() {
// Result para operaciones que pueden fallar con un error específico
let resultado: Result<i32, &str> = Ok(42);
let error: Result<i32, &str> = Err("algo salió mal");
// Option para valores que pueden no existir
let presente: Option<&str> = Some("hola");
let ausente: Option<&str> = None;
// Ambos son enums normales — se manejan con match
match resultado {
Ok(n) => println!("Éxito: {n}"),
Err(e) => println!("Error: {e}"),
}
match presente {
Some(s) => println!("Valor: {s}"),
None => println!("Sin valor"),
}
}El operador `?`: propagación elegante de errores
El operador ? es la pieza más importante del manejo de errores en Rust. Hace dos cosas:
- Si el valor es
Ok(T), extraeTy continúa - Si el valor es
Err(E), convierte el error (viaFrom) y retorna inmediatamente
use std::fs;
use std::io;
// Sin ? — verboso y ruidoso
fn leer_archivo_verboso(ruta: &str) -> Result<String, io::Error> {
let contenido = match fs::read_to_string(ruta) {
Ok(s) => s,
Err(e) => return Err(e),
};
Ok(contenido.to_uppercase())
}
// Con ? — limpio y directo
fn leer_archivo(ruta: &str) -> Result<String, io::Error> {
let contenido = fs::read_to_string(ruta)?;
Ok(contenido.to_uppercase())
}
// Encadenando múltiples operaciones que pueden fallar
fn procesar_numero(s: &str) -> Result<f64, Box<dyn std::error::Error>> {
let n: i32 = s.trim().parse()?; // ParseIntError → Box<dyn Error>
let raiz = if n >= 0 {
(n as f64).sqrt()
} else {
return Err("No se puede calcular raíz de negativo".into());
};
Ok(raiz)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
match procesar_numero(" 16 ") {
Ok(r) => println!("Raíz: {r}"),
Err(e) => println!("Error: {e}"),
}
match procesar_numero("abc") {
Ok(r) => println!("Raíz: {r}"),
Err(e) => println!("Error: {e}"),
}
Ok(())
}Tipos de error personalizados
Para proyectos reales, querrás crear tipos de error descriptivos:
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
EntradaInvalida { campo: String, mensaje: String },
BaseDeDatos(String),
Red { codigo: u16, url: String },
Parse(ParseIntError),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::EntradaInvalida { campo, mensaje } => {
write!(f, "Entrada inválida en '{campo}': {mensaje}")
}
AppError::BaseDeDatos(msg) => write!(f, "Error de base de datos: {msg}"),
AppError::Red { codigo, url } => {
write!(f, "Error de red {codigo} al conectar con {url}")
}
AppError::Parse(e) => write!(f, "Error de parseo: {e}"),
}
}
}
// Implementar std::error::Error para AppError
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Parse(e) => Some(e),
_ => None,
}
}
}
// Conversión automática desde ParseIntError
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn validar_edad(s: &str) -> Result<u8, AppError> {
let edad: i32 = s.parse()?; // ParseIntError se convierte via From
if edad < 0 || edad > 150 {
return Err(AppError::EntradaInvalida {
campo: String::from("edad"),
mensaje: format!("{edad} no es una edad válida (0-150)"),
});
}
Ok(edad as u8)
}
fn main() {
for entrada in ["25", "abc", "-5", "200"] {
match validar_edad(entrada) {
Ok(edad) => println!("Edad válida: {edad}"),
Err(e) => println!("Error: {e}"),
}
}
}Métodos útiles de Result y Option
fn main() {
let ok: Result<i32, &str> = Ok(42);
let err: Result<i32, &str> = Err("fallo");
// --- Métodos de Result ---
// unwrap: extrae el valor o entra en pánico
println!("{}", ok.unwrap());
// expect: como unwrap pero con mensaje personalizado
println!("{}", ok.expect("Debería ser Ok"));
// unwrap_or: valor por defecto si es Err
println!("{}", err.unwrap_or(0));
// unwrap_or_else: calcular valor por defecto
println!("{}", err.unwrap_or_else(|e| { println!("Error: {e}"); -1 }));
// map: transformar el valor Ok
let doble = ok.map(|n| n * 2);
println!("{:?}", doble);
// map_err: transformar el error
let transformado = err.map_err(|e| format!("ERROR: {e}"));
println!("{:?}", transformado);
// and_then: encadenar Results
let resultado = ok
.and_then(|n| if n > 0 { Ok(n * 10) } else { Err("negativo") })
.and_then(|n| Ok(format!("Resultado: {n}")));
println!("{:?}", resultado);
// is_ok / is_err
println!("¿ok es Ok? {}", ok.is_ok());
println!("¿err es Err? {}", err.is_err());
// --- Métodos de Option ---
let some: Option<i32> = Some(10);
let none: Option<i32> = None;
println!("{}", some.unwrap_or(0));
println!("{}", none.unwrap_or_default()); // i32::default() = 0
// ok_or: convertir Option a Result
let r: Result<i32, &str> = none.ok_or("no hay valor");
println!("{:?}", r);
// filter: conservar Some solo si cumple condición
let par = some.filter(|n| n % 2 == 0);
println!("{:?}", par); // None (10 no es par... espera, sí lo es)
// zip: combinar dos Options
let a = Some(1);
let b = Some("hola");
println!("{:?}", a.zip(b)); // Some((1, "hola"))
}Panic: cuándo y cuándo no usarlo
fn main() {
// panic! para situaciones que "no deberían ocurrir"
// (errores de programación, no errores de usuario)
let v = vec![1, 2, 3];
// v[10]; // panic: index out of bounds
// assert! para precondiciones
fn calcular_raiz(n: f64) -> f64 {
assert!(n >= 0.0, "No se puede calcular raíz de {n}");
n.sqrt()
}
// todo! para código no implementado aún
fn funcion_pendiente() -> i32 {
todo!("Implementar lógica de negocio")
}
// unreachable! para ramas que no deberían alcanzarse
let estado = 1_u8;
let mensaje = match estado {
0 => "inactivo",
1 => "activo",
2 => "suspendido",
_ => unreachable!("Estado inválido: {estado}"),
};
println!("Estado: {mensaje}");
println!("{:.4}", calcular_raiz(16.0));
}La cadena completa: manejo de errores idiomático
use std::collections::HashMap;
#[derive(Debug)]
enum Error {
ClaveFaltante(String),
ValorInvalido { clave: String, valor: String },
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::ClaveFaltante(k) => write!(f, "Clave faltante: '{k}'"),
Error::ValorInvalido { clave, valor } => {
write!(f, "Valor inválido para '{clave}': '{valor}'")
}
}
}
}
fn obtener_numero(config: &HashMap<&str, &str>, clave: &str) -> Result<i64, Error> {
let valor = config.get(clave)
.ok_or_else(|| Error::ClaveFaltante(clave.to_string()))?;
valor.parse::<i64>()
.map_err(|_| Error::ValorInvalido {
clave: clave.to_string(),
valor: valor.to_string(),
})
}
fn main() {
let config: HashMap<&str, &str> = [
("puerto", "8080"),
("timeout", "abc"),
].into_iter().collect();
for clave in ["puerto", "timeout", "host"] {
match obtener_numero(&config, clave) {
Ok(n) => println!("{clave} = {n}"),
Err(e) => println!("Error: {e}"),
}
}
}Con un manejo de errores robusto en tu arsenal, la próxima lección explora módulos y crates — cómo Rust organiza el código en unidades reutilizables.
El operador ? solo funciona en funciones que retornan Result u Option
Si usas ? en main(), debes cambiar la firma a fn main() -> Result<(), Box<dyn std::error::Error>>. Esto permite propagar errores directamente desde main y los imprime automáticamente si ocurren.
Evita unwrap() y expect() en código de producción
unwrap() y expect() entran en pánico si el valor es Err o None. Son útiles en prototipos, tests y cuando tienes certeza absoluta de que el valor existe. En código de producción, propaga el error con ? o manéjalo con match/if let.
use std::num::ParseIntError;
use std::fmt;
// Error personalizado
#[derive(Debug)]
enum ErrorCalculo {
DivisionPorCero,
NumeroInvalido(ParseIntError),
NegativoNoPermitido(i64),
}
impl fmt::Display for ErrorCalculo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DivisionPorCero => write!(f, "División por cero"),
Self::NumeroInvalido(e) => write!(f, "Número inválido: {e}"),
Self::NegativoNoPermitido(n) => {
write!(f, "Negativo no permitido: {n}")
}
}
}
}
impl From<ParseIntError> for ErrorCalculo {
fn from(e: ParseIntError) -> Self {
ErrorCalculo::NumeroInvalido(e)
}
}
fn dividir(a: i64, b: i64) -> Result<i64, ErrorCalculo> {
if b == 0 { return Err(ErrorCalculo::DivisionPorCero); }
if a < 0 { return Err(ErrorCalculo::NegativoNoPermitido(a)); }
Ok(a / b)
}
fn parsear_y_dividir(a: &str, b: &str) -> Result<i64, ErrorCalculo> {
let a: i64 = a.trim().parse()?; // ? convierte via From
let b: i64 = b.trim().parse()?;
dividir(a, b)
}
fn main() {
let casos = [("100", "4"), ("abc", "2"), ("50", "0"), ("-10", "2")];
for (a, b) in &casos {
match parsear_y_dividir(a, b) {
Ok(r) => println!("{a}/{b} = {r}"),
Err(e) => println!("Error: {e}"),
}
}
}
Inicia sesión para guardar tu progreso