Diseño de código confiable en JavaScript | Nicolás Garzón
El código confiable no es código que nunca falla. Es código que:
Reduce estados inválidos.
Detecta problemas cerca del origen.
Comunica contratos.
Falla de forma comprensible.
Permite recuperar cuando existe una alternativa real.
Deja suficiente contexto para diagnosticar.
Texto
Copiar entrada incierta
↓
frontera validada
↓
núcleo predecible
↓
efectos controlados
↓
respuesta o error explícito
Una función debe comunicar qué recibe, qué devuelve y cómo falla.
JavaScript
Copiar function requireProduct ( products, productId ) {
const product = products. find (
( candidate ) => candidate. id === productId,
) ;
if ( ! product) {
throw new ProductNotFoundError ( productId) ;
}
return product;
} El nombre requireProduct comunica que la ausencia no es un resultado normal.
JavaScript
Copiar function findProduct ( products, productId ) {
return (
products. find (
( candidate ) => candidate. id === productId,
) ?? null
) ;
} Las dos APIs son válidas porque expresan contratos distintos.
JavaScript
Copiar const product = parseProduct ( request. body) ; Después de esa línea, el núcleo puede confiar en una forma controlada.
JavaScript
Copiar function calculateTotal ( product ) {
const price = Number ( product. price) ;
} JavaScript
Copiar function calculateTotal ( product ) {
return product. price * product. quantity;
} La complejidad se concentra donde entra la incertidumbre.
Una invariante es una condición que debe mantenerse para que un valor siga siendo válido.
JavaScript
Copiar class InventoryItem {
#stock;
constructor ( initialStock ) {
this . #assertStock ( initialStock) ;
this . #stock = initialStock;
}
decrease ( quantity ) {
this . #assertStock (
this . #stock - quantity,
) ;
this . #stock -= quantity;
}
#assertStock ( value ) {
if ( ! Number. isInteger ( value) || value < 0 ) {
throw new RangeError (
"Stock cannot be negative" ,
) ;
}
}
} La regla no depende de que cada consumidor recuerde comprobarla.
JavaScript
Copiar const request = {
isLoading : false ,
hasError : true ,
data : products,
} ; ¿Puede existir data y error al mismo tiempo? ¿Qué significa isLoading junto con ambos?
JavaScript
Copiar const request = {
status : "error" ,
error,
} ; JavaScript
Copiar switch ( request. status) {
case "idle" :
case "loading" :
case "success" :
case "error" :
} Una representación clara hace más difíciles las combinaciones imposibles.
JavaScript
Copiar function calculateOrderTotal ( items ) {
return items. reduce (
( total, item ) =>
total + item. price * item. quantity,
0 ,
) ;
} La función no lee hora, red, globals ni almacenamiento. Con las mismas entradas produce el mismo resultado.
Pruebas.
Debugging.
Reutilización.
Razonamiento.
No todo puede ser puro. La idea es mantener los efectos en capas reconocibles.
JavaScript
Copiar async function checkout ( input, dependencies ) {
const order = parseOrder ( input) ;
const total = calculateOrderTotal ( order. items) ;
const payment = await dependencies. payments. pay ( {
orderId : order. id,
total,
} ) ;
await dependencies. orders. save ( {
... order,
total,
paymentId : payment. id,
} ) ;
} Validación y cálculo son claros. Red y persistencia aparecen como dependencias explícitas.
JavaScript
Copiar function createReport ( data ) {
globalLogger. info ( "Creating report" ) ;
return globalFormatter. format ( data) ;
} JavaScript
Copiar function createReport (
data,
{ logger, formatter } ,
) {
logger. info ( "Creating report" ) ;
return formatter. format ( data) ;
} Las dependencias dejan de estar ocultas y pueden sustituirse durante pruebas.
JavaScript
Copiar function createProduct ( input ) {
const product = {
name : input. name,
price : input. price,
} ;
} JavaScript
Copiar function createProduct ( input ) {
const product = parseProduct ( input) ;
return product;
} Un error cerca del origen suele tener mejor contexto y evita que el programa construya estados parciales.
JavaScript
Copiar throw new RepositoryUnavailableError (
"Product query timed out" ,
{ cause } ,
) ; Texto
Copiar No pudimos cargar los productos. Inténtalo nuevamente.No ocultes el fallo interno, pero tampoco expongas detalles que el usuario no puede resolver.
Texto
Copiar VALIDATION_FAILED
NOT_FOUND
CONFLICT
UNAVAILABLE
TIMEOUT
INTERNAL_ERRORUna taxonomía pequeña permite:
Respuestas consistentes.
Métricas.
Reintentos selectivos.
Mensajes de UI.
Alertas por severidad.
No conviertas cada frase en una categoría distinta.
JavaScript
Copiar try {
return await loadRecommendations ( ) ;
} catch ( error) {
if ( error instanceof RecommendationUnavailableError ) {
return [ ] ;
}
throw error;
} La recomendación es opcional y una lista vacía conserva una experiencia válida.
JavaScript
Copiar try {
return await loadAccountBalance ( ) ;
} catch {
return 0 ;
} Un fallo no significa que el saldo sea cero.
Una operación externa no debería esperar indefinidamente.
JavaScript
Copiar const controller = new AbortController ( ) ;
const timeoutId = setTimeout (
( ) => controller. abort ( ) ,
5000 ,
) ;
try {
return await fetch ( url, {
signal : controller. signal,
} ) ;
} finally {
clearTimeout ( timeoutId) ;
} La implementación concreta depende del entorno. El principio es que quien inicia trabajo debe definir cómo termina o se cancela.
AbortController se estudiará en asincronía y APIs del navegador.
No reintentes automáticamente cualquier fallo.
Una lectura puede ser repetible. Un cobro puede duplicarse si el proveedor procesó la solicitud pero la respuesta se perdió.
Identificar fallos transitorios.
Limitar intentos.
Aplicar espera.
Soportar cancelación.
Diseñar idempotencia.
Registrar el resultado final.
JavaScript
Copiar await saveOrder ( order) ;
await sendConfirmation ( order) ; Si el correo falla, la orden ya existe.
JavaScript
Copiar {
orderStatus : "created" ,
notificationStatus : "pending" ,
} La confiabilidad mejora cuando el estado parcial es visible en lugar de fingir atomicidad.
Una operación idempotente puede repetirse sin producir efectos adicionales distintos al primero.
JavaScript
Copiar await updateOrderStatus (
orderId,
"completed" ,
) ; Puede diseñarse para que repetir el mismo estado no duplique acciones.
Crear un pago, enviar un correo o descontar inventario no son automáticamente idempotentes. Pueden necesitar claves de idempotencia, registros o verificaciones.
JavaScript
Copiar function applyDiscount ( product ) {
return {
... product,
price : product. price * 0.9 ,
} ;
} Crear una referencia nueva puede evitar que una capa cambie silenciosamente datos usados por otra.
No copies todo por costumbre. Comprende qué referencias se comparten y define quién puede modificarlas.
Linters detectan patrones sospechosos.
TypeScript puede verificar contratos estáticos.
Tests comprueban ejemplos y propiedades.
Formatters reducen diferencias de estilo.
Monitoreo muestra fallos reales.
Logs estructurados aportan contexto.
Métricas revelan frecuencia y tendencia.
Ninguna sustituye el diseño. Un tipo correcto puede contener un valor inválido para el dominio; un test puede no cubrir una condición desconocida.
No pruebes únicamente el camino feliz.
Texto
Copiar -1
0
1
máximo permitido
máximo + 1
NaN
Infinity
string numérico
null
undefinedLos límites revelan errores de comparación, coerción y defaults.
Cuando un fallo ocurre en producción necesitas responder:
¿Qué operación falló?
¿Cuándo?
¿Con qué versión?
¿Cuántas veces?
¿A qué request o job pertenece?
¿Qué causa lo originó?
¿Afectó a todos o a un segmento?
Usa identificadores de correlación y datos seguros. No registres secretos.
JavaScript
Copiar function add ( a, b ) {
if ( typeof a !== "number" ) {
}
if ( typeof b !== "number" ) {
}
} Puede ser necesario en una API pública, pero redundante en un núcleo que recibe valores ya validados.
Demasiada validación interna:
Duplica reglas.
Dificulta lectura.
Puede producir mensajes inconsistentes.
Añade costo.
Define fronteras de confianza.
JavaScript
Copiar function parseCreateOrderInput ( input ) {
} JavaScript
Copiar function createOrder ( input ) {
} JavaScript
Copiar async function saveOrder ( order ) {
} JavaScript
Copiar async function handleCreateOrder ( request ) {
try {
const input = parseCreateOrderInput (
request. body,
) ;
const order = createOrder ( input) ;
await saveOrder ( order) ;
return toSuccessResponse ( order) ;
} catch ( error) {
reportError ( error, {
requestId : request. id,
} ) ;
return toPublicResponse ( error) ;
}
} Cada parte tiene una responsabilidad identificable.
Antes de considerar confiable un flujo, pregunta:
¿Dónde entra información no confiable?
¿Qué invariantes deben mantenerse?
¿Cuáles resultados son esperados?
¿Qué fallos deben propagarse?
¿Dónde se registran?
¿Qué mensaje ve el usuario?
¿Qué efectos pueden quedar parciales?
¿La operación puede reintentarse?
¿Cómo se cancela o termina?
¿Qué prueba evita la regresión?
Validar tarde o en todas partes.
Ocultar dependencias en globals.
Modelar estados contradictorios con muchos booleanos.
Retornar fallbacks que cambian el significado.
Reintentar efectos no idempotentes.
Suponer que una excepción revierte operaciones anteriores.
Exponer detalles internos al usuario.
Confiar únicamente en tipos o tests.
Registrar datos sensibles.
Construir capas defensivas sin definir fronteras de confianza.
La confiabilidad comienza con contratos y fronteras claras.
Valida la incertidumbre antes de entrar al núcleo.
Mantén visibles las invariantes.
Reduce estados imposibles mediante modelos explícitos.
Separa cálculo de efectos externos.
Usa fallos internos precisos y respuestas públicas seguras.
Diseña fallbacks, reintentos y estados parciales de forma honesta.
Herramientas, tests y observabilidad complementan el diseño.
Código confiable puede fallar, pero falla de manera entendible y recuperable.
¿Por qué return 0 no es un fallback seguro cuando falla la consulta del saldo?
Respuesta Porque transforma “no pudimos conocer el saldo” en “el saldo es cero”. Son estados distintos y el usuario podría tomar decisiones incorrectas basándose en información falsa.
El siguiente bloque estudia asincronía , donde estos principios se aplican a callbacks diferidos, Event Loop, promesas, cancelación y errores que atraviesan límites temporales.