Debug con propósito: por qué deberías usar assert y assertionFailure en tus builds de desarrollo con Swift

La manera más rápida de darnos cuenta de que hay algo que no funciona como debería es cuando nuestra aplicación hace crash, en ese momento sabemos que algo no esta funcionando como deberia.

Bajo ese mismo concepto, una buena forma de darnos cuenta durante el desarrollo que algun valor esta fuera de rango o un valor esperado no esta ahi es haciendo crash en nuestra aplicacion voluntariamente.

assert, assertionFailure, precondition, preconditionFailure, fatalError

Estas funciones nos permite poder detener nuestra aplicacion («Crash») ayudandonos a detectar estados imposibles en nuestra aplicacion, y cuando digo estados imposibles me refiero a situaciones que no deberia suceder pero suceden, como un identificador nulo cuando tiene que estar disponible, un string vacio cuando tiene que tener valor, una variable que se mantiene nulo cuando no deberia, que no ayudaran a detectar algun error de logica o flujo en nuestro codigo.

Ahora bien, assert, assertionFailure, precondition, fatalError, todas estas funciones realizan la misma accion (Detener la ejecucion de la aplicacion), con ciertas diferencias, y es importante identificar estas diferencias.

Disponible en Dev BuildsDisponible en Release Build
assert
assertionFailure
precondition
preconditionFailure
fatalError

Debug Builds
Durante el desarrollo, digamos al correr nuestra app con xcode o un dev builds, esta compilado con la propiedad Level Optimization con «-O0» y esto nos permite que muchas funciones de desarrollo este disponibles en nuestros builds, y ayudando a que los asserts y assertionsFailure este disponibles en nuestro codigo.

Release Builds
Lo release builds a diferencia de los debug estos tiene la propiedad Level Optimization con «-O» o Owholemodule permitiendo un mayor grados de optimizacion, y eliminando el codigo de asserts entre otras cosas.


Dado estos es muy importante entender que:

Durante el desarrollo o un debug builds cualquiera de estas funciones detendra la ejecución de nuestra:

  • assert
  • assertioFailure
  • precondition
  • preconditionFailure
  • fatalError

Nuestra aplicación en el appstore o cualquier build con release configuración cualquiera de estas funciones detendra la ejecución de nuestra aplicacin:

  • precondition
  • preconditionFailure
  • fatalError

Por lo que tenemos que tener mucho cuidado en que momento utilizamos alguna de estas funciones.

AssertioFailure Ejemplo

enum UserState {
    case loggedIn
    case loggedOut
}

func handleState(_ state: UserState) {
    switch state {
    case .loggedIn:
        print("Hola")
    case .loggedOut:
        print("Adios")
    @unknown default:
        assertionFailure("Valor inesperado.")
    }
}


Si realizamos un cambio en nuestro codigo y agregamos una nueva propiedad, el tener este comportamiento por defecto en nuestro codigo y lanzando un crash en desarrollo nos permitira rapidamente saber que tenemos que realizar un cambio de implementacion.

func handleNotification(_ userInfo: [AnyHashable: Any]) {
    guard let type = userInfo["type"] as? String else {
        assertionFailure("Type es requerido")
        return
    }
    print("notification type: \(type)")
}

Durante el desarrollo nos podemos encontrar que un valor que estamos esperando y que deberia llegar no esta llegando.

El assertionFailure no ayudara a detectar estos posibles errores lo antes posible. Lo bueno es que en caso de que pasemos por alto algun error, la aplicacion no hara crash en produccion.

fatalError Ejemplo

struct Config {
    let apiKey: String
    
    init(apiKey: String) {
        guard !apiKey.isEmpty else {
            fatalError("API key Invalida")
        }
        self.apiKey = apiKey
    }
}

Este es buen ejemplo donde deberiamos usar un fatalError, que si haria crash de nuestra aplicacion en produccion, dado que el apiKey es obligatorio para que nuestra aplicacion funcione como tal.

La moraleja

No todos los crashes son malos. Algunos son recordatorios de que tu código necesita atención.

  • Usa assert y assertionFailure como tus alarmas de desarrollo.
  • Reserva fatalError para condiciones críticas que no pueden ignorarse.
  • Y recuerda: un crash a tiempo puede ahorrarte horas de debugging en producción.

Referencias

https://developer.apple.com/documentation/swift/fatalerror(_:file:line:)

https://developer.apple.com/documentation/swift/assertionfailure(_:file:line:)