wandres.dev
SINTAXIS Y CONTROL · la gramática completa

defer: ejecutar al salir del ámbito

Qué garantiza defer, por qué los bloques se ejecutan en orden inverso, sus usos legítimos para cerrar recursos y restaurar invariantes, y las trampas de captura y de ámbito.

⏱ 15 min

defer responde a una pregunta que todo lenguaje con salidas múltiples tiene que contestar: si una función puede terminar por cinco sitios distintos, ¿cómo garantizas que la limpieza ocurra en los cinco? La respuesta perezosa es repetir el cierre antes de cada return, y funciona hasta que alguien añade el sexto. defer invierte el problema: escribes la limpieza junto a la apertura, una sola vez, y el lenguaje se encarga de ejecutarla salgas por donde salgas.

🎯 Al terminar esta lección sabrás
  • Precisar cuándo se ejecuta un bloque defer y qué salidas cubre.
  • Explicar por qué varios defer se ejecutan en orden inverso al de declaración.
  • Reconocer los usos legítimos: recursos, invariantes y estado temporal.
  • Evitar las trampas de captura de valores y de ámbito dentro de bucles.

La garantía: se ejecuta al salir, siempre

Un bloque defer no se ejecuta donde lo escribes: se ejecuta cuando el flujo abandona el ámbito que lo contiene, y da igual cómo lo abandone. Retorno normal, retorno temprano de un guard, error propagado con throw, break que sale de un bucle: en todos los casos el bloque corre antes de que el ámbito desaparezca.

func leerConfiguracion(desde ruta: String) throws -> [String: String] {
    let manejador = try FileHandle(forReadingFrom: URL(filePath: ruta))
    defer { try? manejador.close() }        // se cierra pase lo que pase

    guard let datos = try manejador.readToEnd() else {
        throw ConfigError.vacio                // aqui tambien cierra
    }
    return try parsear(datos)                  // y aqui, incluso si parsear lanza
}

La ganancia clave es de proximidad: la línea que abre el recurso y la que lo cierra son vecinas. El lector no necesita recorrer la función entera comprobando que todas las salidas limpian, ni el autor futuro tiene que acordarse de nada cuando añada un return en medio. La corrección deja de depender de la disciplina.

ℹ️
Que ambito exactamente

defer se ata al bloque de llaves donde está escrito, no a la función. Uno dentro de un if, de un for o de un do se dispara al salir de ese bloque, no al final del método. Es una precisión que sorprende la primera vez y que resulta muy útil después: puedes acotar la vida de una restauración al fragmento exacto que la necesita.

El orden inverso es la única opción sensata

Cuando hay varios defer en el mismo ámbito, se ejecutan del último declarado al primero. Parece caprichoso hasta que se piensa en términos de recursos anidados:

func operacion() {
    abrirTransaccion()
    defer { cerrarTransaccion() }      // 3.º en ejecutarse

    let archivo = abrirArchivo()
    defer { archivo.cerrar() }         // 2.º en ejecutarse

    bloquear(mutex)
    defer { desbloquear(mutex) }       // 1.º en ejecutarse

    trabajar()
}

Si se ejecutaran en orden de declaración, cerraríamos la transacción mientras el archivo sigue abierto y el mutex tomado: se desmontaría el andamio empezando por la base. El orden inverso es el desmontaje correcto de una pila, el mismo principio por el que un destructor libera lo último adquirido primero. Adquisición hacia dentro, liberación hacia fuera.

flowchart TD
A[Entrar al ambito] --> B[Adquirir recurso 1 y registrar defer 1]
B --> C[Adquirir recurso 2 y registrar defer 2]
C --> D[Adquirir recurso 3 y registrar defer 3]
D --> E[Trabajo o salida temprana o error]
E --> F[Ejecutar defer 3]
F --> G[Ejecutar defer 2]
G --> H[Ejecutar defer 1]
H --> I[Salir del ambito]

Usos legítimos y usos que no lo son

defer brilla cuando hay una acción de apertura con una contraparte obligatoria de cierre. Fuera de ese patrón, casi siempre estorba.

🍊

Cerrar recursos

Ficheros, sockets, contextos gráficos, transacciones de base de datos, cerrojos. Todo lo que se adquiere y debe devolverse.

🧹

Restaurar invariantes

Poner una bandera a cierto al entrar y devolverla a falso al salir, retirar un observador, restaurar un ajuste global que se tocó de forma temporal.

final class Cargador {
    private var estaCargando = false

    func cargar() async throws -> [Articulo] {
        estaCargando = true
        defer { estaCargando = false }   // se restaura incluso si algo lanza
        return try await servicio.pedirArticulos()
    }
}

Lo que no debe hacerse es usar defer como mecanismo de control de flujo o para ordenar lógica de negocio. Un defer que muta el valor que se va a devolver, o que decide algo importante, es una acción a distancia: el lector ve un return y no sospecha que aún queda código por correr. La regla práctica es que un defer debe ser corto, sin condiciones y sin sorpresas; si no cabe en dos o tres líneas, extráelo a una función con nombre y llama a esa función desde el defer.

El ámbito manda: bucles, bloques y capturas

La confusión más frecuente no es sobre qué hace defer, sino sobre cuándo lo hace. El bloque se ata a las llaves más cercanas que lo envuelven, de modo que la misma línea cambia de significado según dónde la coloques:

for fila in filas {
    defer { contador += 1 }     // se ejecuta al final de CADA vuelta
    guard fila.esValida else { continue }
    procesar(fila)
}

do {
    let cerrojo = tomar(mutex)
    defer { soltar(cerrojo) }   // se suelta al cerrar este do, no al final del metodo
    seccionCritica()
}
// aqui el mutex ya esta libre

La segunda trampa es la captura. El bloque no fotografía los valores al declararse: lee las variables cuando se ejecuta, es decir, al salir del ámbito. Por eso el ejemplo siguiente imprime el valor final y no el inicial, un detalle que sorprende a quien espera el comportamiento de un argumento pasado por valor:

var i = 0
defer { print(i) }   // imprime 10, no 0
i = 10

La tercera es que un defer no puede propagar errores hacia fuera ni retornar: no admite try sin más ni return, porque el ámbito ya se está desmontando y no hay nadie a quien entregarle el resultado. De ahí el try? del primer ejemplo. Y un defer colocado tras un return nunca llega a registrarse, así que jamás corre; conviene declararlos siempre lo antes posible, pegados a la adquisición que compensan.

⚠️
Que no se te olvide

Un defer debe ser corto, incondicional y sin sorpresas. Si necesita ramas, comprobaciones o más de tres líneas, extrae el trabajo a una función con nombre y limita el bloque a llamarla. Un defer largo escondido al principio de un método es exactamente el tipo de acción a distancia que la construcción pretendía eliminar.

Defer traslada la correccion de la memoria del programador al lenguaje

Detrás de defer hay una tesis sobre dónde debe vivir la garantía de limpieza. En C esa garantía vive en la cabeza de quien escribe, y de ahí nacen el goto cleanup y décadas de fugas de recursos por un return añadido a las prisas. C++ la trasladó al sistema de tipos con RAII, atándola al destructor de un objeto: potentísimo, pero exige inventar un tipo envoltorio para cada recurso, aunque la limpieza sea una sola línea usada una sola vez. Java la trasladó a la sintaxis con try-finally y luego con try-with-resources, al precio de un bloque que sangra el trabajo real y separa apertura de cierre por todo el cuerpo. defer elige un punto intermedio deliberado: la garantía la impone el lenguaje, pero la unidad de agrupación no es un tipo ni un bloque envolvente, sino la vecindad textual entre adquirir y liberar. Ese detalle es el que hace que el patrón escale, porque el coste de añadir una limpieza correcta es exactamente una línea junto a la que la provocó, y ningún refactor posterior puede separarlas sin que salte a la vista. La lección general trasciende a Swift: cuando una corrección depende de que un humano recuerde algo lejano en el tiempo o en el espacio del código, la solución no es documentarlo mejor ni revisarlo con más cuidado, sino cambiar la estructura para que recordar deje de ser necesario. defer es esa idea reducida a cinco letras.

⚔️ Comprueba el orden y las trampas
  1. Escribe una función con tres defer que impriman uno, dos y tres, y verifica en consola el orden real de ejecución.
  2. Añade un guard con salida temprana antes de los defer y otro después; observa cuáles se ejecutan en cada caso.
  3. Mete un defer dentro de un bucle for de cinco vueltas y luego muévelo justo antes del bucle. Explica la diferencia en el número de ejecuciones.
  4. Declara var i = 0, escribe defer { print(i) }, incrementa i a diez y comprueba qué se imprime al salir.
  5. Reescribe una función que cierre un recurso antes de cada uno de sus tres return, usando un único defer, y compara cuántos puntos de fallo tenía cada versión.