Ir al contenido

Result en Rust

Si Option representa un valor que puede faltar, Result representa una operación que puede fallar. Y, a diferencia de un valor que simplemente no está, un fallo suele venir con un motivo.

Result es otro enum de la biblioteca estándar, esta vez con dos variantes que llevan datos:

enum Result<T, E> {
Ok(T), // éxito, con un valor dentro
Err(E), // fallo, con un error dentro
}

.parse(), que conociste en la página de conversión de tipos, es un buen ejemplo real: convertir texto en un número puede fallar si el texto no es un número de verdad:

fn main() {
let good: Result<i32, std::num::ParseIntError> = "42".parse();
let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good);
println!("{:?}", bad);
}

Salida:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ Pruébalo tú mismo

ParseIntError { kind: InvalidDigit } es el error que define la propia biblioteca estándar de Rust, por eso aparece en inglés: significa que el texto contiene un “dígito no válido”.

fn main() {
let input = "42";
match input.parse::<i32>() {
Ok(number) => println!("Convertido: {number}"),
Err(e) => println!("No se pudo convertir: {e}"),
}
}
▶ Pruébalo tú mismo

Tiene la misma forma que manejar un Option con match, solo que la rama Err te entrega un valor de error de verdad, que puedes examinar o mostrar, y no un simple “aquí no había nada”.

Escribir tu propia función que devuelve Result

Sección titulada «Escribir tu propia función que devuelve Result»
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("no se puede dividir entre cero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Resultado: {result}"),
Err(e) => println!("Error: {e}"),
}
match divide(10.0, 0.0) {
Ok(result) => println!("Resultado: {result}"),
Err(e) => println!("Error: {e}"),
}
}

Salida:

Resultado: 5
Error: no se puede dividir entre cero
▶ Pruébalo tú mismo

Devolver Result<f64, String> obliga a todo el que llame a la función a tener en cuenta que puede fallar. No hay forma de tratar por accidente una división entre cero como si fuera un número normal.

Si encadenas varias operaciones que pueden fallar, acabas con bloques match metidos unos dentro de otros. El operador ? lo aplana. Significa: “si esto es Err, devuelve ese error ahora mismo desde la función actual; si no, dame el valor que hay dentro de Ok”.

fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> {
let number = input.parse::<i32>()?; // sale de la función si la conversión falla
Ok(number * 2)
}
fn main() {
println!("{:?}", parse_and_double("21"));
println!("{:?}", parse_and_double("oops"));
}

Salida:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ Pruébalo tú mismo

Sin ?, esa línea necesitaría su propio match solo para sacar number o salir antes con el error. ? hace exactamente eso con un solo carácter. Eso sí, solo funciona dentro de una función que también devuelve un Result (o un Option), porque así tiene a dónde devolver el error.

Rust también tiene panic!, que detiene el programa al instante; lo verás con más detalle en la siguiente página. Como regla general: usa Result para los fallos que quien llama podría querer manejar (una entrada incorrecta, un archivo que no existe), y deja panic! para situaciones que indican un error real en el código (un bug), en las que seguir adelante sería peor que parar.

En cuanto tus funciones pueden fallar de varias maneras distintas, un simple error String deja de ser suficiente. La página de errores personalizados te enseña a definir tu propio tipo de error, y la de anyhow y thiserror te muestra los crates que usa la mayoría de los proyectos de Rust para ahorrarse el código repetitivo.

Escribe fn parse_age(input: &str) -> Result<u32, String>. Debe devolver Err con un mensaje útil si el texto no es un número, o si el número pasa de 150. Pruébala con "34", "abc" y "200". Pista: usa match sobre input.parse::<u32>(), y dentro de la rama Ok usa un if para comprobar el tamaño.

Ver solución
fn parse_age(input: &str) -> Result<u32, String> {
match input.parse::<u32>() {
Ok(age) => {
if age > 150 {
Err(format!("{age} es demasiado para ser una edad real"))
} else {
Ok(age)
}
}
Err(_) => Err(format!("'{input}' no es un número")),
}
}
fn main() {
for input in ["34", "abc", "200"] {
match parse_age(input) {
Ok(age) => println!("Edad: {age}"),
Err(e) => println!("Error: {e}"),
}
}
}

Salida:

Edad: 34
Error: 'abc' no es un número
Error: 200 es demasiado para ser una edad real
▶ Pruébalo tú mismo

Escribe fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>, que convierta los dos strings en números y devuelva su suma. Usa ? en lugar de match. Imprime el resultado para ("10", "20") y para ("10", "veinte"). Pista: necesitas un ? por cada parse, y la última línea envuelve la suma en Ok(...).

Ver solución
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
fn main() {
println!("{:?}", add_strings("10", "20"));
println!("{:?}", add_strings("10", "veinte"));
}

Salida:

Ok(30)
Err(ParseIntError { kind: InvalidDigit })
▶ Pruébalo tú mismo

ParseIntError { kind: InvalidDigit } es cómo Rust muestra el error con {:?}: el tipo de fallo es “dígito no válido”.

  • Result<T, E> representa una operación que puede salir bien (Ok) o fallar (Err) con un valor de error.
  • match maneja los dos casos de forma explícita, igual que con Option.
  • El operador ? devuelve el Err automáticamente, así que no necesitas un match después de cada llamada que puede fallar.
  • Prefiere Result a panic! para los errores de los que quien llama podría recuperarse.

Comprueba lo aprendido

1. ¿Cuáles son las dos variantes de Result<T, E>?

2. ¿Qué hace el operador ? dentro de una función que devuelve un Result?

3. Según esta página, ¿cuándo conviene usar Result en lugar de panic!?

4. En el ejemplo de divide de esta página, ¿qué devuelve divide(10.0, 0.0)?

5. ¿Por qué el operador ? solo funciona dentro de una función que también devuelve un Result (o un Option)?

Puntuación: 0 / 5