Veröffentlicht am 19. August 2026 · 4 Min. Lesezeit
Das Panel schrieb „Gespeichert" über Inhalte, die nicht in der Datenbank waren
Sie öffnen zwei Browser-Tabs des Panels. Im einen löschen Sie ein Projekt. Im anderen, früher geöffneten, ändern Sie beim selben Projekt den Termin und klicken auf „Speichern". Das Panel schreibt „Gespeichert".
Es hat nicht gespeichert. Es gab nichts, wohin.
Warum die Datenbank nicht protestiert
UPDATE projekte SET termin = ... WHERE id = ... auf eine Zeile, die es nicht mehr gibt, ist eine völlig korrekte Abfrage. Die Datenbank tut genau das, worum man sie gebeten hat: Sie durchsucht die Tabelle, findet null passende Zeilen, ändert null Zeilen und meldet Erfolg. Es gibt hier keinen Fehler zu melden — keine Übereinstimmung ist kein Ausfall, sondern ein Ergebnis.
Code, der so aussieht:
await sql`UPDATE projekte SET termin = ${termin} WHERE id = ${id}`;
return Response.json({ ok: true });
ist in seinem ok: true immer wahr. Die Abfrage ist gelungen. Nur sind „die Abfrage ist gelungen" und „die Daten stehen in der Datenbank" zwei verschiedene Sätze, und dieser Code verwechselt sie.
Wie ich es gefunden habe
Nicht in der Produktion — bei mir, in einem Trockendurchlauf. Ich habe eine einfache Sonde geschrieben: Lege für jede Art von Datensatz im Panel einen an, lösche ihn, und versuche dann eine Änderung zu speichern, als wäre ein zweiter Tab noch offen. Danach prüfe, was die Route geantwortet hat.
Ergebnis: neun von sechzehn Datensatzarten antworteten {"ok":true}. Neun Stellen, an denen das Panel dem Nutzer „Gespeichert" über Inhalte geschrieben hätte, die es nirgends gibt.
Das ist genau die Fehlerkategorie, die man durch Herumklicken nicht findet. Um von Hand darauf zu stoßen, braucht man zwei offene Tabs und muss den Datensatz genau zwischen dem Laden und dem Speichern löschen. Bei einer Person und einem Tab kommt dieses Szenario nie vor — bis zu dem Tag, an dem es vorkommt.
Die Reparatur ist kleiner, als sie scheint
Der Reflex sagt: „Prüfen wir zuerst, ob der Datensatz existiert" — also ein SELECT vor jedes UPDATE setzen. Das hieße eine Abfrage mehr bei jedem Speichern im ganzen Panel.
Es stellte sich heraus, dass dieses SELECT an den meisten Stellen bereits dort war. Die Hauptdatensätze lesen ihren Zustand ohnehin vor dem Speichern, weil sie ein Änderungsprotokoll führen („was hat sich von was zu was geändert"). Es genügte, das Ergebnis zu verwenden, das schon in einer Variablen lag:
const vorher = await hole(id);
if (!vorher) return antwortKeinDatensatz("projekt");
Eine Bedingung, null neue Abfragen. Wo der Lesevorgang tatsächlich fehlte, kam einer dazu — und das ist ein Preis, den es wert ist, damit die Meldung nicht lügt.
Eine bewusste Ausnahme
DELETE hat das nicht bekommen. Etwas zu löschen, das es bereits nicht mehr gibt, endet in genau dem Zustand, um den gebeten wurde: Dieses Etwas ist nicht da. Eine Meldung „nicht gefunden" wäre hier formal korrekt und praktisch sinnlos — der Nutzer wollte, dass der Datensatz weg ist, und der Datensatz ist weg.
Diese Asymmetrie ist wichtiger, als sie aussieht: Regeln lohnen sich dort, wo sie etwas ändern, und nicht überall, weil sie eine Regel sind. Die Prüfung der Konsequenz halber auch in DELETE einzubauen, sähe nach Ordnung aus und würde Fehlermeldungen in Situationen erzeugen, in denen nichts Schlimmes passiert ist.
Was das nicht repariert
Diese Änderung löst die Kollision zweier Tabs nicht — sie hört nur auf, darüber zu lügen. Wenn jemand im zweiten Tab den Datensatz geändert statt gelöscht hat, überschreibt mein Speichern weiterhin fremde Arbeit. Bewusst: Das ist ein System für eine Person, eine Nebenläufigkeitskontrolle wäre mit Kanonen auf Spatzen geschossen.
Aber auch hier hat sich eines geändert — die Abweichung ist nicht mehr unsichtbar. Der Tab hängt an das Speichern einen Zeitstempel aus dem Moment des Ladens, und der Server vergleicht ihn mit dem aktuellen. Bei einem Unterschied speichert er trotzdem, ergänzt die Antwort aber um einen Satz, dass zwischenzeitlich jemand diesen Datensatz angefasst hat. Die Unterscheidung klingt kleinlich und ist der ganze Unterschied zwischen „das System hat sich geirrt" und „das System hat mich gewarnt und ich habe entschieden".
Der Schluss, den ich mitnehme
Die gefährlichsten Fehler in Bürosystemen bringen die Anwendung nicht zum Absturz. Sie antworten „erledigt". Eine Rechnung, die „rausgegangen" ist und nicht rausging; eine Adresse, die „gespeichert" wurde und die alte blieb; ein Auftrag, im System bestätigt und im Lager nicht vorhanden. In fünf Jahren Büroleitung habe ich diese Fehlerklasse von der anderen Seite gesehen — als „das habe ich doch gestern eingetragen". Jetzt weiß ich, wie sie von innen aussieht.
Seit ich das geprüft habe, behandle ich jede Erfolgsmeldung als eine Behauptung, die zu beweisen ist: Auf welcher Grundlage ist dieser Text erschienen? Wenn die Antwort lautet „weil die Abfrage keinen Fehler geworfen hat", ist das zu wenig.

Patryk Piecyk
Warschau · Junior-Implementierungsberater · ab sofort verfügbar
Siebeneinhalb Jahre habe ich in einem deutschen Unternehmen gearbeitet, fünf davon habe ich sein Büro geführt: Aufträge, Rechnungen, Reklamationen, ERP. Seit Juni 2026 baue ich eigene Werkzeuge für genau diese Arbeit — ich bin kein gelernter Programmierer; der Code entsteht gemeinsam mit einem KI-Assistenten, Entwurf, Entscheidungen und Tests sind meine. Diese Notizen beschreiben, was in diesen Systemen kaputtging und was dabei herauskam.
Eine Frage zu diesem Text?
Schreiben Sie mir →