Analiza eșecului
Când bugul îmbracă mesajul căii de rezervă
Un tracker de colete raporta credențiale lipsă pentru fiecare colet. Credențialele erau la locul lor, iar o clauză except prea largă transforma fiecare încercare de corecție în aceeași propoziție liniștitoare.
Publicat
O eroare și un rezultat gol nu spun același lucru
Un tracker multi-curier care rula pe un server raporta același lucru pentru fiecare colet primit: lipsesc credențialele. E o afirmație precisă. Spune că respectivul cod a rulat, a ajuns în punctul în care avea nevoie de un secret, s-a uitat și n-a găsit nimic. Citit așa, e o treabă de operare — pui credențialele unde le caută trackerul — și nu pare urgentă, fiindcă nimic nu e stricat, doar neconfigurat.
Nimic din toate astea nu era adevărat. Credențialele existau. Codul nu ajungea niciodată în punctul sugerat de mesaj. Trackerul crăpa în rutina care încarcă un helper, iar eroarea era prinsă și convertită într-un rezultat gol, pe care apelantul îl raporta mai departe ca lipsă de date.
Distanța dintre cele două citiri — a crăpat față de n-a găsit nimic — e tot articolul. O eroare arată spre o linie de cod. Un rezultat gol arată spre o bază de date, un fișier de configurare, un proces de operare, sistemul altcuiva. Prima e o investigație scurtă. A doua înseamnă zile de muncă.
Două cauze suprapuse
Trackerul își calcula calea către un helper urcând un număr fix de directoare de la propriul fișier. În arborele de surse în care fusese scris, calculul era corect. Pe server fișierele erau puse plat, așa că același calcul urca peste vârf și producea o cale la rădăcina sistemului de fișiere — o cale care nu avea cum să existe.
Corecția n-a fost ingenioasă: cauți într-o listă de locuri candidate și iei primul care există. Un script vecin, din același director, făcea deja exact asta. Codul vechi nu era greșit când a fost scris; încorpora o presupunere despre structura de fișiere, iar deployul a rupt presupunerea în tăcere, fără să atingă codul.
A doua cauză stătea sub prima. Trackerul cerea de la depozitul de credențiale o singură intrare, care să le țină pe toate. Intrarea aceea nu existase niciodată. Depozitul le ține separat, câte o intrare pentru fiecare curier. Nu lipsea nimic; trackerul cerea un nume pe care nu-l scrisese nimeni, niciodată.
Ambele cauze dau exact același simptom, deci repari doar una și nu se schimbă nimic din ce se vede din afară. Merită spus apăsat, fiindcă am reparat și ieșirea a rămas la fel se citește în mod normal ca dovadă că diagnosticul era greșit.
Unde am greșit: trei încercări, un singur rezultat vizibil
Au fost trei încercări. Prima corecție apela o funcție dintr-un modul care nu era importat în fișierul acela. La rulare, asta ridică o eroare de nume — în Python, un NameError, aruncat în momentul în care linia se execută, nu la încărcarea fișierului.
Eroarea de nume apărea înăuntrul blocului try al căii de rezervă, iar handlerul acelei căi era except Exception: continue. O eroare de nume este o excepție. Deci corecția a rulat, a eșuat în tăcere, bucla a trecut la următorul candidat, iar trackerul a raportat exact același mesaj despre credențiale lipsă ca înainte.
Două încercări consecutive au arătat identic din afară. Concluzia firească, în punctul acela, e că diagnosticul e greșit și cauza reală e în altă parte. Concluzia asta era ea însăși greșită. Diagnosticul fusese corect de ambele dăți; dovada la care ne uitam era produsă de handler, nu de sistem. Citeam părerea căii de rezervă despre lume și o tratam ca pe o observație despre lume.
Schimbarea care a contat a fost îngustarea handlerului la singura eroare pentru care există calea de rezervă: un fișier care nu e acolo. Restul se propagă acum. La rularea următoare a apărut eroarea de nume, iar eroarea de nume a indicat linia.
Clauza except trebuie să numească eroarea pentru care a fost scrisă
Regula care iese din asta e destul de îngustă cât să fie aplicată mecanic. O clauză except de pe o cale de rezervă trebuie să numească singura eroare pe care calea de rezervă a fost scrisă să o suporte, și nimic altceva.
Un handler larg pus acolo nu face sistemul robust. Convertește orice bug din blocul try în mesajul propriu al căii de rezervă. Bugul poate fi o greșeală de tastare, un import lipsă, un atribut redenumit, un număr greșit de argumente — toate ies pe partea cealaltă îmbrăcate în aceeași propoziție.
Asimetria e cea care costă. Cine scrie calea de rezervă are în minte un singur caz benign, iar mesajul pe care îl scrie e calm: date indisponibile, sursă goală, nu s-a găsit nimic. Toți cei care citesc apoi ieșirea moștenesc așteptarea aceea, inclusiv autorul. Să prinzi orice excepție înseamnă să pretinzi că ai anticipat orice eroare din bloc, iar asta aproape nimeni n-a intenționat să pretindă.
Cum faci aceeași verificare
Caută în cod handlerele de excepții largi al căror corp e un continue, un pass sau un return cu valoare goală — listă goală, dicționar gol, null, șir gol. Corpul e indiciul, nu clauza: un handler larg care loghează excepția și o re-aruncă mai departe e altceva și, de obicei, e în regulă.
Pentru fiecare rezultat, întreabă pentru ce eroare anume a fost scris și îngustează clauza la acea eroare. Dacă nu poți numi eroarea, handlerul nu tratează nimic. Ascunde ceva, iar ce ascunde e următorul bug care nimerește în bloc.
Apoi verifică prin efect, nu prin absență. Rulează și cere un rezultat real — un colet cu un status atașat — nu absența unui mesaj de eroare. Pe tot parcursul incidentului procesul s-a terminat curat și a tipărit o propoziție care se citea ca o explicație; o verificare de tipul n-a crăpat a fost adevărată tot timpul și nu ne-a spus nimic.
Limite
Îngustarea unui handler scoate la suprafață erori care înainte erau invizibile. În ziua în care faci deploy, asta arată ca o regresie: stack trace-uri noi, adesea în cod pe care nu l-ai atins. Nu e o regresie, e handlerul care își face treaba, iar deployul trebuie planificat cu asta în minte, nu dat înapoi la primul traceback.
Exemplele de aici vin dintr-un singur script. Nu am măsurat cât de răspândit e tiparul în restul codului, iar articolul nu pretinde nicio rată. Tiparul nu ține de limbaj, deși sintaxa exactă diferă de la un limbaj la altul.
Îngustarea cere și ea judecată, iar judecata poate greși și în cealaltă direcție. Numești excepția prea strâns și o eroare benignă legitimă începe să se propage în producție. Testul e aceeași întrebare ca mai sus: poți spune cu voce tare pentru ce eroare anume există calea de rezervă? Când răspunsul se schimbă, se schimbă și clauza.
Întrebări și răspunsuri
De ce e o idee proastă să prinzi orice excepție pe o cale de rezervă?
Să prinzi orice excepție pe o cale de rezervă e o idee proastă pentru că handlerul larg convertește orice bug din blocul try în mesajul căii de rezervă. O greșeală de tastare, un import lipsă, un atribut redenumit sau un număr greșit de argumente ies toate cu aceeași propoziție calmă, de tipul „date indisponibile”. Prinderea largă pretinde, fără intenție, că ai anticipat orice eroare din bloc.
Cum găsesc handlerele de excepții periculoase într-un proiect?
Handlerele de excepții periculoase sunt cele largi al căror corp e un continue, un pass sau un return cu valoare goală: listă goală, dicționar gol, null, șir gol. Corpul e indiciul, nu clauza. Un handler larg care loghează excepția și o re-aruncă e, de obicei, în regulă. Pentru fiecare handler suspect, numește eroarea anume pentru care a fost scris și îngustează clauza la ea.
De ce corecția mea nu a schimbat mesajul de eroare?
Corecția poate să nu schimbe mesajul de eroare atunci când o clauză except largă înghite noua eroare și tipărește din nou mesajul căii de rezervă. Un NameError aruncat în blocul try rămâne o excepție, deci bucla trece mai departe în tăcere. Două cauze suprapuse cu același simptom ascund și ele o reparație parțială. Îngustarea handlerului lasă eroarea reală să apară și să indice linia.
Apariția unor stack trace-uri noi după îngustarea unui handler înseamnă regresie?
Stack trace-urile noi apărute după îngustarea unui handler nu înseamnă regresie, ci semnul că handlerul își face treaba. Erori invizibile până atunci ies la suprafață, adesea în cod pe care nu l-a atins nimeni. Deployul trebuie planificat cu asta în minte, nu dat înapoi la primul traceback. Totuși, o excepție numită prea strâns poate lăsa o eroare benignă să ajungă în producție.
Cum verific dacă o corecție de bug chiar a funcționat?
O corecție de bug se verifică prin efect, nu prin absența unei erori. Rulezi codul și ceri un rezultat real, de exemplu un colet cu un status atașat. O verificare care confirmă doar că procesul nu a crăpat poate rămâne adevărată tot timpul, în timp ce un handler larg ascunde eșecul în spatele unei propoziții care se citește ca o explicație.
Cum se citează
Going Stellar (2026). Când bugul îmbracă mesajul căii de rezervă. GOING STELLAR S.R.L..
https://goingstellar.ro/ro/cercetare/cand-eroarea-imbraca-mesajul-caii-de-rezerva/