La Ansiedad por las Aserciones

La mayoría de los codebase de producción caen en uno de dos bandos. El bando A trata a assert como un condimento decorativo, espolvoreándolo cada dos líneas hasta que la función se lee como un contract legal escrito por un abogado paranoico. El bando B trata las aserciones como ruedines de entrenamiento solo para desarrollo, eliminándolas todas en tiempo de build y esperando que el código funcione en producción porque los tests pasaron una vez.

Ambos bandos están equivocados. La pregunta no es si usar assert. La pregunta es qué significa realmente una aserción.

Una aserción no es manejo de errores. No es validación de entrada. No es una sugerencia educada. Una aserción es una afirmación de que algo es imposible. Si la aserción se dispara, tu modelo mental del programa está roto. Esa distinción determina todo acerca de dónde pertenecen las aserciones y cuántas deberías escribir.

Las Aserciones Son para Invariants, No para Errores

Cuando un usuario pasa una edad negativa a tu API, eso es un error. Los errores son esperados. Los errores merecen un manejo real, logging y mensajes orientados al usuario. Cuando tu cálculo interno produce un conteo negativo de filas de base de datos después de una query supuestamente exitosa, eso es una violación de invariant. Eso nunca debería suceder. Para eso existen las aserciones.

Esto suena obvio hasta que lees código de producción. He visto funciones que aseguran que una cadena no está vacía, y tres líneas después verifican if (!str) y lanzan una excepción formateada. El desarrollador usó ambas herramientas para la misma condición porque nunca decidió cuál era el contract real.

Aquí está la regla. Si la condición puede ser disparada por entrada externa, no es una aserción. Si solo puede ser disparada por un bug en tu propio código, lo es.

def process_payment(user_id: str, amount_cents: int) -> Receipt:
    # NOT an assertion. Users or upstream services can send bad data.
    if amount_cents <= 0:
        raise ValueError("amount_cents must be positive")

    # NOT an assertion. The user_id comes from the outside world.
    if not user_id:
        raise ValueError("user_id is required")

    receipt = _charge_card(user_id, amount_cents)

    # THIS is an assertion. If charge_card returned None after
    # succeeding, our understanding of the universe is wrong.
    assert receipt is not None, "charge_card succeeded but returned None"

    # THIS is an assertion. A receipt with zero items after a
    # successful charge means our internal logic is broken.
    assert len(receipt.items) > 0, "receipt has no items after successful charge"

    return receipt

Las primeras dos verificaciones protegen el boundary. Las últimas dos protegen la consistencia interna del sistema. Mezclarlas crea confusión sobre quién es responsable de qué.

El Tope de Tres Aserciones

Si te encuentras escribiendo más de tres aserciones en una sola función, tienes uno de dos problemas. O tu función hace demasiadas cosas, o tus invariantes son demasiado vagos para hacer cumplir.

Una función con doce aserciones no es defensiva. Es incierta. El autor no confía en el código que la llama, el código que ella llama, ni en los datos que fluyen entre ellos. Esa incertidumbre debería resolverse mediante refactor, no agregando más declaraciones assert.

El límite práctico viene de lo que un desarrollador puede mantener en su cabeza. Una función debería tener un contract claro. Ese contract implica un pequeño número de invariantes. Si necesitas una docena de aserciones para sentirte seguro, tu función probablemente ha absorbido responsabilidades que pertenecen a otro lugar.

Divide la función. Extrae la parte que transforma datos. Extrae la parte que llama a servicios externos. Dale a cada función extraída su propio conjunto pequeño de invariantes. Tres aserciones por función es una luz de advertencia. Cinco es un pinchazo.

Aserciones en Producción: ¿Activadas o Desactivadas?

Diferentes lenguajes toman diferentes decisiones. Python elimina las declaraciones assert cuando corres con la bandera -O. Los compilers de C y C++ eliminan rutinariamente las aserciones en builds de release. JavaScript no tiene assert integrado en absoluto. O lo polyfillas o usas una biblioteca que permanece activa en producción.

Esto crea un dilema genuino. Si eliminas las aserciones, pierdes la red de seguridad exactamente cuando más la necesitas. Los bugs que solo aparecen en producción corromperán silenciosamente los datos en lugar de fallar rápidamente. Si las mantienes, arriesgas colapsar un proceso de producción por una condición que, aunque teóricamente imposible, en realidad no es fatal.

La respuesta depende del costo de continuar. Si violar el invariant significa que la siguiente operación corromperá la base de datos o filtrará datos sensibles, la aserción debería colapsar el proceso. Una parada dura es mejor que una brecha silenciosa. Si violar el invariant significa una entrada de log ligeramente incorrecta o un glitch menor en la UI, regístralo y continúa.

// This should probably crash. Continuing with a null user
// after auth succeeded is a security hole waiting to happen.
assert(user !== null, "auth middleware returned null user after success");

// This should probably not crash. A stale cache timestamp
// is annoying but not dangerous.
if (cache.timestamp > Date.now()) {
  logger.warn("cache timestamp is in the future, ignoring");
}

No todo invariant merece la misma postura. Aprende a distinguir entre “esto debe detenerse” y “esto es raro pero sobrevivible”.

Lo Que Intentamos y No Funcionó

Al principio de un proyecto, intentamos asegurar cada precondición de función. Cada argumento era verificado por null, tipo, rango y formato. El resultado fue predecible. Los tests pasaron maravillosamente. Producción se cayó la primera vez que una API de terceros devolvió un campo como string en lugar de un número.

El problema no era la aserción. El problema era que aseguramos datos fuera de nuestro control, y luego compilamos con aserciones activadas en producción. Una respuesta externa malformada mató nuestro proceso en lugar de ser sanitizada y manejada. Habíamos construido un sistema que era internamente consistente y externamente frágil.

Aprendimos a separar el boundary del interior. En el boundary, parsea y valida agresivamente. Convierte el caos externo en certeza interna. Dentro del boundary, asegura los invariantes que definen esa certeza. Las aserciones se quedaron. La validación de entrada se movió a funciones de parsing explícitas que devolvieron Result types en lugar de lanzar excepciones.

Una Lista de Verificación Práctica

Antes de agregar una aserción, revisa esta lista:

  1. ¿Puede la entrada externa disparar esto? Si sí, usa validación, no aserción.
  2. Si esto se dispara en producción, ¿debería detenerse el proceso? Si no, registra una advertencia en su lugar.
  3. ¿Esta función ya tiene tres o más aserciones? Si sí, considera refactorizar antes de agregar otra.
  4. ¿Esta aserción seguirá teniendo sentido para alguien que lea el código dentro de seis meses? Las aserciones oscuras se eliminan en los refactors. Las claras sobreviven.

Las aserciones son una herramienta de comunicación tanto como una herramienta de seguridad. Le dicen al siguiente desarrollador, “esta condición es imposible por diseño”. Si la condición en realidad no es imposible por diseño, la aserción está mintiendo. Y las mentiras en código de producción son caras.

Preguntas Frecuentes

¿Debería asegurar los argumentos de función?

Solo si el que llama también es tu código y el argumento es producto de lógica interna, no entrada externa. Las funciones de API públicas deberían validar. Las funciones helper privadas pueden asegurar invariantes sobre los valores que reciben.

¿Qué pasa con TypeScript? Ya atrapa los nulls en tiempo de compilación.

El sistema de tipos de TypeScript es una capa de aserción poderosa, pero desaparece en runtime. Úsalo para todo lo que el compiler puede probar. Agrega aserciones en runtime para los vacíos: respuestas de API, datos deserializados, y cualquier cast as que evite el type checker.

¿Las aserciones afectan el rendimiento?

En la mayoría de los lenguajes, una aserción bien colocada cuesta microsegundos. Si estás asegurando dentro de un tight loop procesando millones de items, mueve la aserción fuera del loop. Verifica el invariant sobre el batch, no sobre cada elemento.

¿Debería escribir funciones de assert personalizadas?

Solo cuando el mensaje de aserción integrado sería inútil. Un assertNonEmpty personalizado que imprime la longitud real del arreglo es más útil que un assert len(items) > 0 genérico que se cae sin contexto. Mantenlos pequeños. No construyas un assertion framework.