Error E0373 de Rust: closure may outlive the current function (usa move)
E0373 significa que una closure (función anónima) toma prestada una variable local, pero la closure podría seguir ejecutándose (o seguir existiendo) después de que termine la función dueña de esa variable. Normalmente te lo encuentras la primera vez que llamas a thread::spawn o devuelves una closure desde una función. La buena noticia: el compilador ya te dice la solución, y casi siempre es una sola palabra, move. Esta página muestra primero la solución y luego explica el porqué. Para los demás errores de esta serie, mira Errores del compilador de Rust.
El mensaje de error
Sección titulada «El mensaje de error»error[E0373]: closure may outlive the current function, but it borrows `scores`, which is owned by the current function --> src/main.rs:5:32 |5 | let handle = thread::spawn(|| { | ^^ may outlive borrowed value `scores`6 | println!("Scores: {scores:?}"); // ❌ solo toma prestado scores | ------ `scores` is borrowed here |note: function requires argument type to outlive `'static` --> src/main.rs:5:18 |5 | let handle = thread::spawn(|| { | __________________^6 | | println!("Scores: {scores:?}"); // ❌ solo toma prestado scores7 | | }); | |______^help: to force the closure to take ownership of `scores` (and any other referenced variables), use the `move` keyword |5 | let handle = thread::spawn(move || { | ++++En español: “la closure puede vivir más que la función actual, pero toma prestado scores, cuyo dueño es la función actual”. La ayuda final propone usar la palabra clave move para que la closure se quede con scores.
Este es el programa que lo provoca:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(|| { println!("Scores: {scores:?}"); // ❌ solo toma prestado scores }); handle.join().unwrap();}En código async verás lo mismo con palabras un poco distintas: “async block may outlive the current function, but it borrows user_id”. Es el mismo error, y tiene la misma solución (async move).
Qué significa
Sección titulada «Qué significa»Lee las etiquetas de arriba abajo:
^^ may outlive borrowed value(“puede vivir más que el valor prestado”) señala la closure||. Rust no puede prometer que la closure haya terminado antes de quemainsiga adelante.`scores` is borrowed here(“scoresse toma prestado aquí”) señala el lugar donde la closure usascores. Comoprintln!solo necesita leerlo, la closure capturó una referencia ascores, no el vector en sí.function requires argument type to outlive 'static(“la función exige que el tipo del argumento viva al menos tanto como'static”) explica por qué eso es un problema:thread::spawnsolo acepta closures que puedan vivir todo lo que el programa las necesite.'staticsignifica “no toma prestado nada que pueda desaparecer”.
Imagina que le prestas tu cuaderno a un amigo y le dices “devuélvemelo cuando acabes”, justo cuando estás a punto de mudarte a otra ciudad. Si tu amigo trabaja despacio, el cuaderno ya no tiene casa a la que volver. Rust rechaza ese trato y te pide que, en lugar de prestarlo, regales el cuaderno.
Por qué Rust hace esto
Sección titulada «Por qué Rust hace esto»Una closure captura las variables de la forma más barata que funcione. Si solo lee una variable, la toma prestada con &. Eso es perfecto para closures que se ejecutan enseguida, como las que pasas a .map() o .filter(). Puedes leer más en la página de Closures.
Un hilo nuevo es diferente. Puede ejecutarse durante cualquier cantidad de tiempo, y main no lo espera a menos que llames a join. Si el hilo tuviera una referencia a scores y main terminara primero, scores se liberaría y el hilo leería memoria ya liberada. Eso es una “dangling reference” (referencia colgante), uno de los fallos más comunes en el código con hilos de C y C++. Rust lo detiene al compilar.
Lo mismo pasa cuando devuelves una closure. Las variables locales de la función (y sus parámetros) se liberan cuando la función termina. Una closure que todavía las tome prestadas apuntaría a la nada.
Cómo solucionarlo
Sección titulada «Cómo solucionarlo»Solución 1: Añade move (la respuesta habitual)
Sección titulada «Solución 1: Añade move (la respuesta habitual)»Elige esta casi siempre. Es exactamente lo que sugiere rustc. move le dice a la closure que tome el ownership (propiedad) de las variables que usa, para que viajen con ella:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(move || { let total: i32 = scores.iter().sum(); println!("Puntuaciones: {scores:?}, total {total}"); }); handle.join().unwrap();}Salida:
Puntuaciones: [90, 85, 77], total 252La misma solución sirve para una función que devuelve una closure. Sin move, |x| x + n toma prestado el parámetro n, que desaparece en cuanto make_adder termina. Con move, cada closure guarda su propia copia de n:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n}
fn main() { let add_five = make_adder(5); let add_ten = make_adder(10); println!("{}", add_five(1)); println!("{}", add_ten(1));}Salida:
611move no cambia qué trait de closure obtienes. Una closure move que solo lee sus datos sigue siendo Fn y se puede llamar muchas veces.
Solución 2: Clona primero, si todavía necesitas el valor
Sección titulada «Solución 2: Clona primero, si todavía necesitas el valor»Después de move, la variable pertenece a la closure. Si main la usa más tarde, te saldrá E0382 (“borrow of moved value”, uso de un valor movido), y rustc sugiere clonar el valor antes de moverlo. Haz una copia para la closure y quédate con el original:
use std::thread;
fn main() { let guests = vec![String::from("Ana"), String::from("Ben")]; let guests_for_thread = guests.clone();
let handle = thread::spawn(move || { println!("El hilo ve {} invitados", guests_for_thread.len()); }); handle.join().unwrap();
println!("main todavía tiene: {guests:?}");}Salida:
El hilo ve 2 invitadosmain todavía tiene: ["Ana", "Ben"]Clonar una lista pequeña es barato y está perfectamente bien. Si los datos son grandes, o muchos hilos los necesitan, usa Arc (Solución 3) para que todos compartan una sola copia.
Solución 3: Comparte con Arc entre varios hilos
Sección titulada «Solución 3: Comparte con Arc entre varios hilos»Elige esta cuando varios hilos necesitan leer los mismos datos. Arc (“atomically reference counted”, con contador de referencias atómico) permite que muchos dueños compartan un mismo valor. Arc::clone solo copia un puntero y aumenta un contador; los datos en sí no se copian:
use std::sync::Arc;use std::thread;
fn main() { let prices = Arc::new(vec![4.5, 2.0, 3.5]); let mut handles = Vec::new();
for discount in [0.0, 0.1, 0.5] { let prices = Arc::clone(&prices); // un Arc para cada hilo handles.push(thread::spawn(move || { let total: f64 = prices.iter().sum(); total * (1.0 - discount) })); }
for handle in handles { println!("Total: {:.2}", handle.join().unwrap()); } println!("Precios: {prices:?}");}Salida:
Total: 10.00Total: 9.00Total: 5.00Precios: [4.5, 2.0, 3.5]Arc por sí solo permite compartir en modo de solo lectura. Si los hilos también necesitan modificar los datos, envuélvelos como Arc<Mutex<T>>. Mira Concurrencia y Estado compartido.
Solución 4: Toma prestado con seguridad usando std::thread::scope
Sección titulada «Solución 4: Toma prestado con seguridad usando std::thread::scope»Elige esta cuando los hilos solo necesitan los datos para un trabajo corto y prefieres tomarlos prestados en lugar de moverlos o clonarlos. Los hilos que se lanzan dentro de thread::scope tienen garantizado terminar antes de que scope regrese, así que tomar prestadas variables locales es seguro y no hace falta move:
use std::thread;
fn main() { let scores = vec![90, 85, 77, 64]; let (first, second) = scores.split_at(2);
let (a, b) = thread::scope(|s| { let h1 = s.spawn(|| first.iter().sum::<i32>()); let h2 = s.spawn(|| second.iter().sum::<i32>()); (h1.join().unwrap(), h2.join().unwrap()) }); // aquí los dos hilos ya terminaron
println!("{a} + {b} = {}", a + b); println!("Sigue siendo nuestro: {scores:?}");}Salida:
175 + 141 = 316Sigue siendo nuestro: [90, 85, 77, 64]Los hilos con ámbito (scoped threads) son la respuesta más limpia para “reparte el trabajo y luego usa los resultados”. La lección de Estado compartido empieza con ellos.
Dónde lo verás normalmente
Sección titulada «Dónde lo verás normalmente»Lanzar hilos en un bucle. Incluso un simple número se toma prestado si la closure solo lo lee:
for worker in 1..=3 { handles.push(thread::spawn(|| { println!("Worker {worker} started"); // ❌ toma prestado `worker` }));}Solución: thread::spawn(move || ...). worker es un i32, así que move simplemente lo copia en cada hilo.
Devolver una closure en una caja. Box::new(|| greeting.clone()) dentro de una función que devuelve Box<dyn Fn() -> String> toma prestada la variable local greeting, y rustc añade note: closure is returned here (la closure se devuelve aquí). Solución: Box::new(move || greeting.clone()). Más en Fn, FnMut y FnOnce.
Tareas async (Tokio y compañía). tokio::spawn funciona como thread::spawn: la tarea puede ejecutarse mucho después de que la función actual termine, así que necesita datos 'static. Si escribes tokio::spawn(async { ... }) y usas una variable local dentro, obtienes “async block may outlive the current function”. Solución: tokio::spawn(async move { ... }), y clona o haz Arc::clone de todo lo que todavía necesites fuera de la tarea. Mira Async/await.
Errores relacionados
Sección titulada «Errores relacionados»- E0382: borrow of moved value: aparece justo después de añadir
movesi luego vuelves a usar la variable fuera de la closure. Clónala primero (Solución 2) o usaArc(Solución 3). - E0597: borrowed value does not live long enough: un primo cercano. Una referencia sigue en uso después de que el valor al que apunta se liberó.
- E0515: cannot return reference to local variable: el mismo problema de “desaparece cuando la función termina”, con una referencia normal en lugar de una closure.
- E0106: missing lifetime specifier: aparece cuando intentas devolver una referencia (o un tipo de closure) que toma prestado algo.
- Todos los errores del compilador: la lista completa de páginas de errores.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. La closure de un hilo imprime name sin usar move. ¿Por qué obtienes E0373?
2. Añades move, pero main todavía necesita imprimir el vector después de lanzar el hilo. ¿Cuál es la solución más sencilla?
3. ¿Qué herramienta permite a los hilos tomar prestadas variables locales sin move?
Puntuación: 0 / 3
Para leer más
Sección titulada «Para leer más»- E0373 — Rust error codes index (en inglés): la explicación oficial de este error, con la solución de
move. - Using Threads to Run Code Simultaneously — The Rust Book (en inglés): la sección “Using move Closures with Threads” recorre exactamente este error.
- Closures — The Rust Book (en inglés): cómo capturan las closures las variables: por referencia, por referencia mutable o por valor.
- Palabra clave
move— documentación de la biblioteca estándar (en inglés): qué hacemovecon lo que captura una closure, con ejemplos cortos. std::thread::scope— documentación de la biblioteca estándar (en inglés): hilos con ámbito que pueden tomar prestados datos locales con seguridad.