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

guard: la salida temprana

Por qué guard produce código más legible que anidar condicionales: el contrato de salida obligatoria, el desempaquetado que sobrevive al bloque y el camino feliz sin sangrado.

⏱ 15 min

guard es la construcción que mejor resume la filosofía de Swift: en vez de comprobar si todo va bien y meter el trabajo dentro del if, declaras por adelantado lo que exiges y te marchas si no se cumple. El resultado es un cuerpo de función donde las precondiciones viven arriba, en fila, y el trabajo real queda al nivel de sangrado cero. No es azúcar sobre if: el compilador impone un contrato que if no puede imponer.

🎯 Al terminar esta lección sabrás
  • Conocer el contrato de guard y por qué su else debe abandonar el ámbito.
  • Comparar el anidamiento creciente del if con la lista plana de precondiciones.
  • Entender por qué lo que guard let desempaqueta sigue vivo después del bloque.
  • Saber cuándo guard deja de aportar y if es la herramienta correcta.

El contrato: si no se cumple, te vas

guard evalúa una condición y, si es falsa, ejecuta su else. Hasta ahí es un if invertido. La diferencia está en la regla que el compilador impone sobre ese else: está obligado a salir del ámbito actual. Con return, throw, break, continue o una llamada que nunca retorne, como fatalError. Si el bloque termina cayendo hacia abajo, no compila.

func registrar(nombre: String, edad: Int) throws {
    guard !nombre.isEmpty else {
        throw ValidacionError.nombreVacio
    }
    guard edad >= 18 else {
        throw ValidacionError.menorDeEdad
    }
    // A partir de aqui el nombre no esta vacio y la edad es valida.
    // Y esto no es una suposicion: es una garantia estructural.
    almacen.insertar(Usuario(nombre: nombre, edad: edad))
}

Esa obligación es justo lo que da valor a la construcción. Con un if que comprueba lo contrario, nada impide que alguien olvide el return y el flujo continúe con datos inválidos; con guard esa negligencia es un error de compilación. La condición deja de ser una recomendación y pasa a ser una precondición verificada.

ℹ️
El else de guard no es opcional ni prescindible

No existe un guard sin else, y su bloque no puede quedar vacío. El lenguaje te obliga a responder a la pregunta incómoda: ¿qué hago cuando esto no se cumple? Muchos errores nacen de no haberla contestado nunca. Aquí no se puede posponer.

De la pirámide a la banda plana

Compara las dos formas de escribir la misma lógica. Primero, con condicionales anidados, cada requisito empuja el trabajo un nivel más a la derecha:

func procesar(_ datos: Data?) -> String? {
    if let datos = datos {
        if let texto = String(data: datos, encoding: .utf8) {
            if !texto.isEmpty {
                return texto.uppercased()
            } else {
                return nil
            }
        } else {
            return nil
        }
    } else {
        return nil
    }
}

Con guard los requisitos se alinean arriba y el resultado queda solo, sin sangrado y sin ruido:

func procesar(_ datos: Data?) -> String? {
    guard let datos else { return nil }
    guard let texto = String(data: datos, encoding: .utf8) else { return nil }
    guard !texto.isEmpty else { return nil }
    return texto.uppercased()
}

Lo que ha cambiado no es solo la cantidad de líneas. En la primera versión, entender qué devuelve la función obliga a rastrear con qué if empareja cada else, un trabajo que crece con el cuadrado de la profundidad. En la segunda, el lector aplica una lectura secuencial: cada línea añade una garantía y ninguna la quita. El camino feliz es la última línea, siempre al mismo nivel, y los caminos de fallo están agrupados donde ocurre cada uno.

flowchart TD
A[Entrada a la funcion] --> B{Precondicion 1}
B -->|Falla| S1[Salir con error o nil]
B -->|Cumple| C{Precondicion 2}
C -->|Falla| S2[Salir con error o nil]
C -->|Cumple| D{Precondicion 3}
D -->|Falla| S3[Salir con error o nil]
D -->|Cumple| E[Camino feliz sin sangrado]

El desempaquetado que sobrevive

Aquí está la ventaja técnica que ninguna reordenación de if puede reproducir. Cuando if let desempaqueta un opcional, el nombre nuevo solo existe dentro de las llaves del if. Cuando lo hace guard let, el nombre existe desde esa línea hasta el final del ámbito que lo contiene.

func saludar(a usuario: Usuario?) -> String {
    guard let usuario else { return "Hola, invitado" }

    // usuario ya no es opcional en TODO el resto de la funcion
    let apodo = usuario.apodo ?? usuario.nombre
    let saludo = "Hola, \(apodo)"
    return saludo
}

Es una consecuencia lógica del contrato: si el else está obligado a abandonar el ámbito, entonces todo el código que sigue solo puede alcanzarse cuando la condición fue cierta, y el compilador puede afinar el tipo a partir de ahí. La forma abreviada guard let usuario sin el = usuario de la derecha, disponible desde Swift 5.7, evita además el ritual de repetir el mismo nombre tres veces.

🍊

if let

Ata el valor al bloque. Ideal cuando el desempaquetado solo interesa para una rama breve y el resto de la función sigue igual.

🧭

guard let

Ata el valor al resto del ámbito. Ideal cuando sin ese valor la función no tiene nada que hacer.

Un solo guard admite además varias condiciones separadas por comas, mezclando desempaquetados y comprobaciones booleanas; cada una puede usar lo que ataron las anteriores. Y con guard case se aplica cualquier patrón, igual que en un switch:

func publicar(_ borrador: Borrador?) throws {
    guard let borrador,
          let titulo = borrador.titulo,
          !titulo.isEmpty,
          borrador.palabras >= 300
    else { throw PublicacionError.incompleto }

    guard case .revisado(let revisor) = borrador.estado else {
        throw PublicacionError.sinRevisar
    }

    almacen.publicar(borrador, titulo: titulo, revisadoPor: revisor)
}

Cuatro requisitos y un patrón, una sola salida, cero sangrado añadido. La versión con condicionales anidados de esta misma función tendría cinco niveles de profundidad y cinco else que emparejar a ojo.

Cuándo guard estorba

guard no es siempre la respuesta. Su lógica es “si esto falla, aquí no hay nada más que hacer”, y forzarlo donde ambas ramas son igual de legítimas produce código peor. Si tienes dos caminos simétricos, ambos con trabajo real, un if con else dice la verdad y un guard la disfraza de excepción. Si necesitas diez guard seguidos, el problema no es el flujo sino el diseño: probablemente esa función hace demasiado, o el tipo de entrada admite estados que no deberían ser representables.

⚠️
No conviertas un else legitimo en una falsa excepcion

Un guard cuyo else contiene quince líneas de lógica de recuperación ha invertido la jerarquía: lo excepcional ocupa el espacio y lo normal queda escondido. Cuando el bloque de salida crece más que el camino feliz, vuelve a if o extrae la recuperación a su propia función.

Cada guard estrecha el universo de estados posibles

La virtud de guard no es estética, es epistemológica: reordena dónde vive el conocimiento dentro de una función. Con condicionales anidados, lo que sabes en un punto del código depende de la ruta que te llevó hasta él, y esa ruta es invisible en la línea que estás leyendo; hay que reconstruirla mentalmente subiendo por las llaves, y el esfuerzo crece con la profundidad. Con guard, el conocimiento es acumulativo y local: cualquier línea sabe, sin mirar arriba, que todas las precondiciones anteriores se cumplen, porque el compilador ha demostrado que el flujo no puede llegar hasta ella de otro modo. Es la misma idea que sostiene la programación por contrato de Meyer o las precondiciones de Dijkstra, solo que verificada por el sistema de tipos en vez de por un comentario. Y el efecto secundario es el más valioso: cada guard que escribes es una hipótesis explícita sobre tus datos, así que la lista de guard al inicio de una función se convierte en su especificación legible. Si esa lista se vuelve larga, no has escrito mala lógica de control: has descubierto que tu tipo de entrada permite demasiados estados inválidos, y la solución correcta no es más guard, sino un tipo mejor que haga imposibles esos estados y deje esas comprobaciones sin sentido.

⚔️ Aplana una funcion
  1. Escribe una función que reciba tres opcionales y devuelva un resultado combinado, primero con if let anidados y luego con guard let. Cuenta niveles de sangrado en cada versión.
  2. Quita el return del bloque else de un guard y lee el error exacto del compilador.
  3. Comprueba que una variable desempaquetada con if let no existe fuera del bloque, y que con guard let sí.
  4. Convierte un guard cuyo else tenga diez líneas en un if con else, y decide cuál de las dos versiones prefieres y por qué.
  5. Usa guard dentro de un bucle for saliendo con continue, y explica por qué ahí return no sería lo correcto.