Zwanzig Kontakte sollten in einem CRM archiviert werden, doch die Benutzeroberfläche blendete ein grünes Erfolgs-Badge ein, während die Operation im Stillen nichts tat. Ein TypeScript-Helper, der das Feld error bei Supabase-JS-Mutationen zur Pflicht macht, zwingt Entwickler nun dazu, sich diesem Fehler zu stellen, anstatt ihn unter den Teppich zu kehren.
Warum die Rückgabestruktur von Supabase-JS eine Falle ist
Der JavaScript-Client von Supabase (@supabase/supabase-js) wirft keine Exceptions, wenn ein Datenbank-Schreibvorgang abgelehnt wird. Stattdessen löst er das Promise mit einem Objekt { data, error } auf. Wenn eine Row-Level-Security (RLS)-Policy, eine Verletzung eines Unique-Keys oder eine andere Einschränkung die Abfrage blockiert, wird data als null zurückgegeben und error enthält die Datenbankmeldung. Der Client geht davon aus, dass der Aufrufer error prüft; er bricht den Aufruf niemals ab.
In der Praxis behandeln viele Codebasen den Aufruf als eine „Fire-and-Forget“-Operation:
await supabase
.from('contacts')
.update({ statut: 'ancien', archived_at: now })
.eq('id', id)
return { ok: true }
Wenn das update durch eine RLS-Regel blockiert wird, wird das Promise dennoch aufgelöst. Da der Entwickler { error } nie destrukturiert, bleibt das Scheitern unsichtbar. Die Funktion gibt { ok: true } zurück, die UI zeigt Erfolg an, und die Daten bleiben unverändert. Es erscheinen keine Logs, kein Sentry-Alert wird ausgelöst, und der Bug kann tagelang unentdeckt bleiben.
Ein Linter reicht nicht aus
Statische Analyse-Tools können warnen, wenn eine error-Eigenschaft ignoriert wird, aber sie können keinen Laufzeit-Vertrag erzwingen. Ein Entwickler kann immer noch const _ = await … schreiben und die Warnung unterdrücken, oder er kann einen Kommentar hinzufügen, um die Regel zu deaktivieren. Das zugrunde liegende Problem ist, dass das Typsystem einen erfolgreichen Aufruf erlaubt, ohne jemals error zu erwähnen.
Der mutate()-Helper: Fehlerbehandlung zur Pflicht machen
Der Autor hat einen kleinen Wrapper namens mutate() entwickelt, der die Struktur des Rückgabetyps ändert. Anstatt { data, error } gibt der Helper ein Tupel [data, error] zurück, bei dem error ein Pflichtfeld ist. TypeScript verweigert dann die Kompilierung jedes Aufrufs, der das zweite Element verwirft.
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
}
Die Verwendung wird explizit:
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 }
Wenn ein Entwickler vergisst, err zu erfassen, gibt TypeScript einen Fehler aus: „Tuple type [T, any] of length 2 has no element at index 1“. Der Code lässt sich nicht kompilieren, bis der Fehler behoben wurde. Der Helper injiziert zudem eine Sentry-Instrumentierung, die garantiert, dass jede Ablehnung der Datenbank protokolliert wird, selbst wenn der Aufrufer sie später ignoriert.
Was auf dem Spiel steht
- Datenintegrität – Stille Fehler lassen ungültige Zustände in die Produktion gelangen. Ein archivierter Kontakt, der die aktive Liste nie verlassen hat, kann dazu führen, dass nachgelagerte Berichte falsch sind.
- Nutzervertrauen – Eine UI, die Erfolg vorgibt, während sich nichts geändert hat, untergräbt das Vertrauen. Kunden sehen „archiviert“, finden den Kontakt aber weiterhin in Suchergebnissen.
- Entwicklerzeit – Die Jagd nach Phantom-Bugs kostet Stunden. Explizite Fehlerbehandlung macht das Problem direkt am Ort des Fehlers sichtbar und verkürzt die Debugging-Schleife.
- Betriebskosten – Das Hinzufügen einiger Zeilen Code und eines kleinen Wrappers ist vernachlässigbar im Vergleich zu den Kosten eines Vorfalls durch stillen Datenverlust.
Der Kompromiss
Der Helper erhöht die Verbosity: Jeder Mutationsaufruf gibt nun ein Tupel zurück, und die Aufrufer müssen einen if (err)-Block schreiben. Manche Teams mögen dies als „Boilerplate-Rauschen“ betrachten, insbesondere bei einfachen CRUD-Aktionen, bei denen sie Erfolg erwarten. Das Gegenargument ist, dass der zusätzliche Code eine Sicherheitsmaßnahme ist und kein optionales Feature. In Umgebungen, in denen Datenkorrektheit oberste Priorität hat – CRM-Systeme, Finanzen, Gesundheitswesen – zahlt sich das Erzwingen der Prüfung aus.
Wie es weitergeht
Der Autor verspricht einen dritten Teil, der RLS-Policies untersucht, die ohne Erklärung null Zeilen zurückgeben. Dieses Muster verbirgt, genau wie der Fall der stillen Fehler, Fehler hinter einer scheinbar erfolgreichen Abfrage. Zusammen zielen die Teile darauf ab, die „Gerüchte“-Seite der Supabase-API aufzudecken und Entwicklern konkrete Werkzeuge an die Hand zu geben, um Fakten einzufordern.
Fazit: Das Design von Supabase-JS lässt einen fehlgeschlagenen Schreibvorgang wie einen Erfolg aussehen, es sei denn, man denkt daran, das error-Feld zu lesen. Indem man Aufrufe in einen TypeScript-gestützten Helper kapselt, der error zur Pflicht macht und an Sentry protokolliert, verwandelt man stille Fehler in sichtbare, handlungsrelevante Ereignisse. Die geringfügige Erhöhung der Codegröße erkauft Ihnen Datenzuverlässigkeit und Nutzervertrauen – zwei Dinge, die ein stiller Erfolg niemals liefern kann.
