Se suponía que veinte contactos debían archivarse en un CRM, pero la interfaz de usuario mostró un distintivo verde de éxito y la operación no hizo nada en silencio. Un helper de TypeScript que hace que el campo error sea obligatorio en las mutaciones de Supabase-JS ahora obliga a los desarrolladores a enfrentar ese fallo en lugar de ocultarlo bajo la alfombra.
Por qué la estructura de retorno de Supabase-JS es una trampa
El cliente de JavaScript de Supabase (@supabase/supabase-js) no lanza excepciones cuando se rechaza una escritura en la base de datos. En su lugar, resuelve la promesa con un objeto { data, error }. Si una política de seguridad a nivel de fila (RLS), una violación de clave única o cualquier otra restricción bloquea la consulta, data regresa como null y error contiene el mensaje de la base de datos. El cliente asume que quien realiza la llamada inspeccionará el error; nunca aborta la llamada.
En la práctica, muchas bases de código tratan la llamada como una operación de tipo "dispara y olvida":
await supabase
.from('contacts')
.update({ statut: 'ancien', archived_at: now })
.eq('id', id)
return { ok: true }
Cuando la update es bloqueada por una regla de RLS, la promesa aún se resuelve. Debido a que el desarrollador nunca desestructura { error }, el fallo es invisible. La función devuelve { ok: true }, la interfaz muestra éxito y los datos permanecen sin cambios. No aparecen registros, no se dispara ninguna alerta de Sentry y el error puede permanecer ahí durante días.
Un linter no es suficiente
Las herramientas de análisis estático pueden advertir cuando se ignora una propiedad error, pero no pueden imponer un contrato en tiempo de ejecución. Un desarrollador aún puede escribir const _ = await … y silenciar la advertencia, o puede añadir un comentario para suprimir la regla. El problema subyacente es que el sistema de tipos permite que una llamada tenga éxito sin mencionar nunca el error.
El helper mutate(): haciendo obligatorio el manejo de errores
El autor construyó un pequeño wrapper llamado mutate() que cambia la estructura del tipo de retorno. En lugar de { data, error }, el helper devuelve una tupla [data, error] donde error es un campo obligatorio. TypeScript entonces se niega a compilar cualquier llamada que descarte el segundo elemento.
async function mutate<T>(promise: Promise<{ data: T | null; error: any }>) {
const { data, error } = await promise
// Send error to Sentry immediately
if (error) Sentry.captureException(error)
return [data, error] as const
}
El uso se vuelve explícito:
const [result, err] = await mutate(
supabase
.from('contacts')
.update({ statut: 'ancien', archived_at: now })
.eq('id', id)
)
if (err) {
// Handle or rethrow
return { ok: false, message: err.message }
}
return { ok: true, data: result }
Si un desarrollador olvida capturar err, TypeScript emite un error: “Tuple type [T, any] of length 2 has no element at index 1.” El código no compilará hasta que se aborde el error. El helper también inyecta instrumentación de Sentry, garantizando que cada rechazo de la base de datos se registre incluso si el llamador lo ignora más tarde.
Lo que está en juego
- Integridad de los datos – Los fallos silenciosos permiten que estados inválidos se filtren en producción. Un contacto archivado que nunca salió de la lista activa puede hacer que los informes posteriores sean incorrectos.
- Confianza del usuario – Una interfaz de usuario que afirma tener éxito mientras nada cambia erosiona la confianza. Los clientes ven "archivado" pero siguen encontrando el contacto en las búsquedas.
- Tiempo del desarrollador – Perseguir errores fantasma consume horas. El manejo explícito de errores saca a la luz el problema en el punto de falla, acortando el ciclo de depuración.
- Costo operativo – Añadir unas pocas líneas de código extra y un pequeño wrapper es insignificante comparado con el costo de un incidente de pérdida de datos silenciosa.
La contrapartida
El helper añade verbosidad: cada llamada de mutación ahora devuelve una tupla, y los llamadores deben escribir un bloque if (err). Algunos equipos pueden ver esto como ruido de código repetitivo (boilerplate), especialmente para acciones CRUD simples donde esperan éxito. El contraargumento es que el código extra es una salvaguarda, no una característica opcional. En entornos donde la corrección de los datos es primordial —sistemas CRM, finanzas, salud—, forzar la comprobación se paga por sí solo.
Qué sigue
El autor promete una tercera entrega que examina las políticas de RLS que devuelven cero filas sin explicación. Ese patrón, al igual que el caso del error silencioso, oculta fallos tras una consulta aparentemente exitosa. Juntas, la serie pretende exponer el lado de los "rumores" de la API de Supabase y dar a los desarrolladores herramientas concretas para exigir hechos.
Conclusión: El diseño de Supabase-JS permite que una escritura fallida parezca un éxito a menos que recuerdes leer el campo error. Al envolver las llamadas en un helper forzado por TypeScript que hace que error sea obligatorio y lo registra en Sentry, conviertes los fallos silenciosos en eventos visibles y accionables. El modesto aumento en el tamaño del código te brinda confiabilidad de datos y confianza del usuario, dos cosas que ningún éxito silencioso podrá ofrecer jamás.
