Los tres errores clásicos: qué te dice el borrow checker de verdad
Cannot borrow as mutable, use of moved value y does not live long enough son los mensajes que más verás. Cada uno no es un castigo sino un diagnóstico con nombre y número: qué pregunta de diseño esconde E0502, E0382 y E0597, cómo leer un move parcial, y cómo usar rustc --explain para traducirlos en vez de sufrirlos.
El borrow checker no habla en acertijos: cada uno de sus rechazos es un diagnóstico con nombre y número de referencia. Tres mensajes concentran la inmensa mayoría de tus primeros choques —cannot borrow as mutable, use of moved value y does not live long enough— y detrás de cada uno hay exactamente una pregunta de diseño mal contestada. Aprender a leer estos errores, en lugar de padecerlos, es la frontera entre pelear con Rust y programar con él.
- Descifrar E0502 como un solapamiento temporal de préstamos que se excluyen.
- Descifrar E0382 como el intento de usar un valor cuyo dueño ya cambió.
- Descifrar E0597 como dos vidas que no encajan, nunca un valor incorrecto.
- Traducir cualquiera de los tres al problema de diseño que denuncia.
El mapa de los tres diagnósticos
Antes de bucear en cada mensaje, retén su forma general. Cada error es la sombra de una de las tres invariantes que el checker nunca deja pasar: no dos accesos incompatibles a la vez, no usar lo que ya cediste, no una referencia que sobreviva a su dato.
E0502 · accesos que chocan
Un préstamo compartido y uno mutable —o dos mutables— vivos a la vez sobre el mismo dato. El arreglo casi nunca es clonar: es ordenar los accesos en el tiempo.
E0382 · el dueño cambió
Usas un valor no-Copy después de moverlo. El compilador no perdió el dato: lo entregó a otro. La pregunta es si esa función necesitaba propiedad o le bastaba &T.
E0597 · vidas que no encajan
Una referencia pretende durar más que el dato al que apunta. No hay ningún valor mal escrito: hay dos ventanas de vida que no se solapan como deberían.
“cannot borrow as mutable”: dos accesos que no pueden convivir
El primero aparece en cuanto pides mutar algo que sigues mirando por otro lado. El caso mínimo:
fn main() {
let mut config = vec!["a".to_string(), "b".to_string()];
let primera = &config[0]; // préstamo compartido: nace aquí
config.push("c".to_string()); // ERROR E0502: necesita &mut config
println!("{primera}"); // ...pero `primera` aún se usa después
}
El mensaje literal es “cannot borrow config as mutable because it is also borrowed as immutable”, y el compilador subraya tres puntos: dónde nace el préstamo compartido, dónde intentas el mutable, y dónde se usa el compartido más tarde. Esa tercera anotación es la clave: el conflicto solo existe porque los dos préstamos se solapan en el tiempo. El checker no te prohíbe leer y mutar config; te prohíbe hacerlo simultáneamente. Lo que de verdad te está diciendo es: “si dejara pasar esto, un push podría realojar el buffer del Vec y primera quedaría apuntando a memoria liberada”. El error no es sintáctico, es una invalidación de iterador detectada antes de ejecutarse.
Y como el conflicto es puramente temporal, la cura casi siempre es puramente temporal: usa el préstamo compartido hasta el final antes de pedir el mutable. Gracias a los préstamos no léxicos, basta con que el último uso del compartido preceda a la mutación para que ambos dejen de solaparse:
fn main() {
let mut config = vec!["a".to_string(), "b".to_string()];
let primera = &config[0];
println!("{primera}"); // ÚLTIMO uso del compartido: aquí muere el préstamo
config.push("c".to_string()); // OK: el &mut ya no se solapa con ningún &
}
Cuando veas E0502, resiste el impulso de clonar el dato para tener dos copias independientes. La pregunta real es: ¿de verdad necesito los dos accesos vivos a la vez? Casi siempre no. Termina de usar el préstamo compartido —lee lo que necesites, cópialo a una variable propia si es Copy— antes de pedir el mutable, y el solapamiento desaparece sin una sola asignación de heap.
“use of moved value”: el valor ya tiene otro dueño
El segundo brota de la semántica de move. Pasar un valor no-Copy a una función transfiere su propiedad; usarlo después es usar algo que ya no es tuyo.
fn registrar(nombre: String) {
println!("registrado: {nombre}");
}
fn main() {
let usuario = String::from("Ada");
registrar(usuario); // el valor se MUEVE a la función
println!("{usuario}"); // ERROR E0382: borrow of moved value: `usuario`
}
El mensaje es “borrow of moved value: usuario”, y viene acompañado de dos anotaciones inseparables: “value moved here” en la llamada, y “value borrowed here after move” en el println!. El compilador incluso sugiere el arreglo mecánico —clonar, o que la función tome &String—, pero lo importante es lo que el error significa: no hay ningún dato perdido ni corrupto. El String está perfectamente vivo, solo que su dueño ahora es el parámetro de registrar, que lo liberará al terminar. Rust te frena porque, si te dejara leer usuario, dos variables creerían poseer el mismo buffer y ambas intentarían liberarlo: el doble free que la semántica de move existe para hacer imposible.
La pregunta de diseño es limpia y siempre la misma: ¿esta función necesita poseer el valor, o le basta con mirarlo? Si solo lo lee, la firma correcta es &String (o mejor &str), y el move no ocurre. Si de verdad necesita poseerlo pero tú lo quieres después, entonces el diseño exige dos dueños, y ahí clone sí es la respuesta honesta.
fn registrar(nombre: &str) { // solo mira: pide una referencia
println!("registrado: {nombre}");
}
fn main() {
let usuario = String::from("Ada");
registrar(&usuario); // se presta, no se mueve
println!("{usuario}"); // OK: `usuario` sigue siendo suyo
}
Una variante que confunde a mucha gente es el move parcial: mover un campo de un struct deja el resto accesible, pero invalida el struct como un todo. El compilador rastrea la propiedad campo a campo:
struct Perfil {
nombre: String,
edad: u32,
}
fn main() {
let p = Perfil { nombre: String::from("Ada"), edad: 36 };
let n = p.nombre; // move PARCIAL: se mueve solo el campo `nombre`
println!("{}", p.edad); // OK: `edad` es Copy y no se movió
// println!("{}", p.nombre); // ERROR E0382: `p.nombre` fue movido
println!("{n}");
}
Puedes leer p.edad porque es Copy y jamás se movió, pero p.nombre ya tiene otro dueño (n), y p como valor completo tampoco puede volver a usarse. El move parcial es coherente con la regla del dueño único aplicada con precisión quirúrgica: la propiedad no es del struct en bloque, sino de cada una de sus partes.
“does not live long enough”: las vidas no encajan
El tercero es el más incomprendido, porque su mensaje habla de tiempo y nosotros leemos como si hablara de valores.
fn main() {
let referencia;
{
let efimero = String::from("vida breve");
referencia = &efimero; // préstamo de algo que va a morir enseguida
} // aquí muere `efimero`: su buffer se libera
println!("{referencia}"); // ERROR E0597: `efimero` does not live long enough
}
“Does not live long enough” no significa que efimero sea incorrecto, sino que su vida es demasiado corta para el uso que le das. El compilador anota dónde muere el dato, dónde nace el préstamo y dónde se usa la referencia después de esa muerte. La solución jamás es forzar la referencia: es alargar la vida del dato —sacarlo del bloque interno— o acortar la de la referencia —usarla antes de que el dato muera—. Cuando leas E0597, no busques un valor mal escrito; busca dos ventanas de vida que no se contienen una a la otra.
flowchart TB err[Un rechazo del borrow checker] --> a[Choque de accesos simultaneos] err --> b[Uso tras mover el valor] err --> c[Referencia mas longeva que su dato] a --> a2[E0502 ordena los accesos en el tiempo] b --> b2[E0382 decide poseer o prestar en vez de mover] c --> c2[E0597 alarga el dato o acorta la referencia]
No tienes que memorizar qué significa cada número. Cada código de error trae una explicación extensa con ejemplos, accesible con rustc --explain E0502 (o E0382, o E0597) desde la terminal. cargo imprime además, al pie de cada error, la pista “for more information, try rustc --explain ...”. Acostúmbrate a leer esas explicaciones: son el manual oficial de por qué el préstamo es ilegal, y cada una te enseña un fragmento del modelo de ownership que luego reconocerás en el siguiente error.
Detente en lo que acabas de aprender a hacer: has convertido tres mensajes crípticos en tres preguntas de diseño precisas —¿necesito estos dos accesos a la vez?, ¿esta función debe poseer o mirar?, ¿esta referencia cabe dentro de la vida de su dato?—. Esas preguntas no las inventó Rust; existen en todo programa que gestiona memoria manualmente. En C se contestan implícitamente, con disciplina y suerte, y contestarlas mal produce un use-after-free que se manifiesta tres semanas después en producción, en la máquina de un cliente, sin stack trace útil. Rust hace algo radical: te obliga a contestarlas ahora, en tu teclado, con el archivo abierto y el contexto fresco, y te da el número de línea exacto. El borrow checker no es un guardián que te niega el paso; es el revisor de código más barato, más rápido y más incorruptible que tendrás jamás, uno que trabaja en el instante en que el arreglo cuesta segundos en vez de semanas. Cuando interiorizas que E0502, E0382 y E0597 son las tres preguntas fundamentales de la gestión de datos formuladas en el momento óptimo, dejas de leerlos como obstáculos y empiezas a leerlos como lo que son: el compilador pensando contigo.
E0502 (cannot borrow as mutable) denuncia dos accesos incompatibles solapados: ordena los accesos en el tiempo, no claves. E0382 (use of moved value) denuncia usar un valor cuyo dueño ya cediste —incluido el move parcial de un campo—: decide si la función debía poseer o solo prestar. E0597 (does not live long enough) denuncia una referencia que sobrevive a su dato: ajusta las vidas, no el valor. Los tres son preguntas de diseño adelantadas al momento más barato de contestarlas, y rustc --explain te da el manual de cada una.
- Reproduce el E0502 del
Vecy localiza los tres puntos que subraya; luego reordénalo para que el préstamo compartido termine antes delpush. - Provoca un E0382 pasando un
Stringa una función; arréglalo primero con&stry explica por qué esa es la mejor cura, y solo después conclone. - Escribe un move parcial de un campo
Stringde un struct y comprueba qué campos siguen siendo legibles y cuáles no. - Reproduce el E0597 del bloque interno y describe qué dos ventanas de vida no encajan.
- Ejecuta
rustc --explain E0382y resume en una frase la pregunta de diseño que, según esa explicación, esconde el error.