Przejdź do treści
Powrót do bloga

Opublikowano 19 sierpnia 2026 · 3 min czytania

Panel pisał „Zapisano” nad treścią, której nie było w bazie

Otwierasz w panelu dwie karty przeglądarki. W jednej kasujesz projekt. W drugiej, otwartej wcześniej, poprawiasz w tym samym projekcie termin i klikasz „Zapisz”. Panel pisze „Zapisano”.

Nie zapisał. Nie miał gdzie.

Dlaczego baza nie protestuje

UPDATE projekty SET termin = ... WHERE id = ... na wiersz, którego już nie ma, to całkowicie poprawne zapytanie. Baza robi dokładnie to, o co ją poproszono: przegląda tabelę, znajduje zero pasujących wierszy, zmienia zero wierszy i zwraca sukces. Nie ma tu błędu do zgłoszenia — brak dopasowania nie jest awarią, tylko wynikiem.

Kod, który wygląda tak:

await sql`UPDATE projekty SET termin = ${termin} WHERE id = ${id}`;
return Response.json({ ok: true });

jest zawsze prawdziwy w swoim ok: true. Zapytanie się udało. Tylko że „zapytanie się udało” i „dane są w bazie” to dwa różne zdania, a ten kod myli je ze sobą.

Jak to znalazłem

Nie z produkcji — u siebie, przy przejściu na sucho. Napisałem prostą sondę: dla każdego rodzaju rekordu w panelu utwórz go, skasuj, a potem spróbuj zapisać zmianę tak, jakby druga karta była wciąż otwarta. Potem sprawdź, co odpowiedziała trasa.

Wynik: dziewięć z szesnastu rodzajów rekordów odpowiadało {"ok":true}. Dziewięć miejsc, w których panel napisałby użytkownikowi „Zapisano” nad treścią, której nigdzie nie ma.

To jest dokładnie ta kategoria błędu, której nie znajdzie się przez klikanie. Żeby na niego trafić ręcznie, trzeba mieć otwarte dwie karty i skasować rekord dokładnie pomiędzy wczytaniem a zapisem. Przy jednej osobie i jednej karcie taki scenariusz nie zdarza się nigdy — do dnia, w którym się zdarzy.

Naprawa jest mniejsza, niż się wydaje

Odruch podpowiada: „sprawdźmy najpierw, czy rekord istnieje” — czyli dołożyć SELECT przed każdym UPDATE. To by znaczyło jedno zapytanie więcej na każdy zapis w całym panelu.

Okazało się, że w większości miejsc ten SELECT już tam był. Rekordy główne i tak czytają swój stan przed zapisem, bo prowadzą log zmian („co się zmieniło z czego na co”). Wystarczyło użyć wyniku, który już leżał w zmiennej:

const przed = await pobierz(id);
if (!przed) return odpowiedzBrakRekordu("projekt");

Jedna instrukcja warunkowa, zero nowych zapytań. Tam, gdzie odczytu faktycznie nie było, doszedł jeden — i to jest cena, którą warto zapłacić za to, żeby komunikat nie kłamał.

Jeden wyjątek, świadomy

DELETE tego nie dostał. Usunięcie czegoś, czego już nie ma, kończy się dokładnie tym stanem, o który proszono: tego czegoś nie ma. Komunikat „nie znaleziono” byłby tu formalnie poprawny i praktycznie bez sensu — użytkownik chciał, żeby rekordu nie było, i rekordu nie ma.

Ta asymetria jest ważniejsza, niż wygląda: reguły warto stosować tam, gdzie coś zmieniają, a nie wszędzie, bo są regułą. Konsekwentne dołożenie sprawdzenia do DELETE wyglądałoby na porządek, a produkowałoby komunikaty o błędach w sytuacjach, w których nic złego się nie stało.

Czego to nie naprawia

Ta zmiana nie rozwiązuje kolizji dwóch kart — tylko przestaje o niej kłamać. Jeśli w drugiej karcie ktoś zmienił rekord zamiast go skasować, mój zapis nadal nadpisze cudzą pracę. Świadomie: to system dla jednej osoby, kontrola współbieżności byłaby armatą na wróbla.

Ale i tu zmieniło się jedno — rozjazd przestał być niewidoczny. Karta dokłada do zapisu znacznik czasu z chwili wczytania, a serwer porównuje go z aktualnym. Przy różnicy zapisuje mimo to, ale dokłada do odpowiedzi zdanie o tym, że w międzyczasie ktoś ruszył ten rekord. Rozróżnienie brzmi drobiazgowo, a jest całą różnicą między „system się pomylił” a „system mnie ostrzegł, a ja zdecydowałem”.

Wniosek, który biorę dalej

Najgroźniejsze błędy w systemach biurowych nie wywalają aplikacji. One odpowiadają „gotowe”. Faktura, która „poszła”, a nie poszła; adres, który „zapisano”, a został stary; zamówienie potwierdzone w systemie i nieistniejące w magazynie. Przez pięć lat prowadzenia biura widziałem tę klasę problemów z drugiej strony — jako „przecież wpisywałam to wczoraj”. Teraz wiem, jak wygląda od środka.

Odkąd to sprawdziłem, każdy komunikat o sukcesie traktuję jak twierdzenie do udowodnienia: na jakiej podstawie ten napis się pojawił? Jeśli odpowiedź brzmi „bo zapytanie nie rzuciło błędem”, to jest za mało.

Patryk Piecyk

Patryk Piecyk

Warszawa · junior konsultant wdrożeniowy · dostępny od zaraz

Przez 7,5 roku pracowałem w niemieckiej firmie, pięć lat prowadząc jej biuro: zamówienia, faktury, reklamacje, ERP. Od czerwca 2026 buduję własne narzędzia do tej samej roboty — nie jestem programistą z wykształcenia, kod powstaje w parze z asystentem AI, a projekt, decyzje i testy są moje. Te notatki opisują, co się w tych systemach zepsuło i co z tego wyszło.

Masz pytanie do tego tekstu?

Napisz do mnie