Ci sono bug che si risolvono in pochi minuti.

Poi ce ne sono altri che sembrano non avere alcun senso.

Il codice sembra corretto.
La logica sembra corretta.
Hai controllato più volte quello che succede. Eppure il programma continua a comportarsi in un modo che non riesci a spiegare. È proprio in questi momenti che si vede la differenza tra provare a risolvere un bug e ragionare da developer. Perché quando un bug sembra impossibile, il problema non è quasi mai trovare una soluzione più velocemente. Il problema è capire che cosa stai ancora dando per scontato.

Quando il bug non ha senso

La prima reazione davanti a un comportamento inspiegabile è spesso questa:

“Ma è impossibile.”

Il valore è quello giusto.
La condizione è corretta.
La funzione viene chiamata.
Quella variabile non può avere quel valore.

Eppure lo ha.

Questo è un momento importante perché se il comportamento osservato contraddice quello che pensi dovrebbe succedere, ci sono solo due possibilità:

  1. Il programma non sta facendo quello che pensi.
  2. La tua interpretazione di quello che sta succedendo è incompleta.

La seconda possibilità è quella che tendiamo a dimenticare.

Quando siamo convinti di aver capito il problema, iniziamo a cercare conferme alla nostra teoria invece di metterla in discussione. Ed è qui che il debugging può diventare un labirinto.

Non cercare subito la soluzione

Quando trovi un bug difficile, la tentazione è modificare il codice.

Provi una condizione diversa.

Aggiungi un controllo.

Sposti una chiamata.

Cambi un valore.

Aggiungi un if.

Poi provi di nuovo.

Se non funziona, torni indietro e provi qualcos’altro.

Dopo mezz’ora potresti aver fatto dieci modifiche senza sapere quale di queste abbia realmente cambiato il comportamento.

Hai lavorato molto, ma hai imparato poco.

Un approccio più efficace parte da una domanda diversa:

“Che cosa so con certezza?”

Non:

“Come posso sistemarlo?”

Prima bisogna capire il problema.

Separare ciò che sai da ciò che pensi

Quando un bug è difficile, prova a dividere le informazioni in due gruppi.

Da una parte ci sono i fatti.

Dall’altra le ipotesi.

Ad esempio:

Fatto: la funzione viene chiamata.

Ipotesi: viene chiamata con i dati corretti.

Fatto: l’API restituisce una risposta.

Ipotesi: quella risposta viene utilizzata nel modo che immagini.

Fatto: una variabile contiene null.

Ipotesi: non dovrebbe mai poter contenere null.

Questa distinzione sembra banale, ma cambia completamente il modo di affrontare il problema.

Perché molte ore di debugging vengono spese cercando di correggere un’ipotesi che non è mai stata verificata.

Un bug non è una spiegazione

Dire:

“Il problema è sicuramente questa funzione.”

non significa aver trovato il problema.

Significa aver formulato un’ipotesi.

E un’ipotesi deve essere verificata.

Questa è una delle abitudini che più cambia il modo di fare debugging.

Invece di modificare direttamente il codice, puoi cercare un modo per smentire la tua teoria.

Se pensi che una funzione riceva un valore sbagliato, controllalo.

Se pensi che una funzione non venga eseguita, verifica che venga eseguita.

Se pensi che il problema sia nell’API, controlla la richiesta e la risposta.

Se pensi che il valore venga modificato in un determinato punto, osserva cosa succede prima e dopo.

L’obiettivo non è dimostrare che hai ragione, ma scoprire se hai torto il prima possibile.

Ridurre il problema

Un bug complesso spesso sembra complesso perché ci stai guardando attraverso tutto il sistema.

Frontend.

Backend.

Database.

API.

Stato dell’applicazione.

Browser.

Librerie.

Configurazioni.

Quando tutto sembra coinvolto, diventa difficile capire dove guardare.

Una delle strategie più potenti è quindi ridurre il problema.

Puoi chiederti:

“Qual è la quantità minima di codice necessaria per riprodurre questo comportamento?”

Se riesci a passare da un’intera applicazione a una singola funzione, hai già fatto un enorme passo avanti.

Se riesci a passare da dieci variabili a due, ancora meglio.

Se riesci a riprodurre il problema con un input specifico, hai eliminato una parte dell’incertezza.

Non stai ancora risolvendo il bug.

Stai rendendo il bug più comprensibile.

Cambiare una variabile alla volta

Quando non sai cosa stia causando un problema, cambiare molte cose contemporaneamente rende il risultato quasi inutile.

Immagina di modificare tre condizioni, cambiare una chiamata API e aggiornare una libreria.

Il bug scompare.

Hai risolto il problema? Non necessariamente.

Non sai quale modifica abbia fatto la differenza.

Potresti aver corretto il problema oppure averlo semplicemente nascosto.

Per questo, quando possibile, conviene modificare una sola variabile alla volta.

È un principio molto semplice, quasi scientifico:

cambi una cosa, osservi il risultato, aggiorni la tua ipotesi.

Il debugging diventa così un processo di esperimenti.

Quando il comportamento cambia, chiediti perché

Uno degli errori più comuni è considerare la scomparsa del bug come la fine dell’indagine.

Ma se hai cambiato qualcosa e improvvisamente funziona, quella modifica contiene un’informazione importante.

Devi capire perché.

Supponiamo che aggiungendo un console.log() il bug sparisca.

Fantascienza! Adoro la fantascienza!

La prima reazione potrebbe essere:

“Ok, era quello.”

Normalmente un console.log() non dovrebbe correggere la logica della tua applicazione.

Quindi la domanda interessante diventa:

“Perché aggiungere quel codice ha cambiato il comportamento?”

Potresti aver scoperto un problema di timing.

Oppure un problema di inizializzazione.

Oppure una condizione di gara.

Oppure semplicemente un effetto collaterale.

La modifica non è necessariamente la soluzione.

Può essere un indizio.

Il bug che scompare non è necessariamente un bug risolto

Esistono bug che sembrano risolversi quando:

  • Riavvii l’applicazione
  • Ricompili
  • Svuoti la cache
  • Aggiorni la pagina
  • Cambi browser
  • Aggiungi un log
  • Cambi l’ordine di esecuzione
  • Aggiorni una dipendenza

Queste situazioni sono particolarmente insidiose.

Perché il risultato ti dà una gratificazione immediata:

“Finalmente funziona.”

Ma se non hai capito la causa, non sai se il problema sia realmente scomparso.

Potrebbe semplicemente essersi spostato.

Un developer non dovrebbe cercare soltanto di ottenere di nuovo il comportamento corretto.

Dovrebbe cercare di capire perché quel comportamento era sbagliato.

Tornare alle basi del comportamento

Quando un problema diventa troppo complicato, spesso cerchiamo una spiegazione altrettanto complicata.

In realtà può essere utile fare il contrario.

Tornare alle domande più semplici:

  • Quale codice viene eseguito?
  • Con quali dati?
  • In quale ordine?
  • Quante volte viene eseguito?
  • Quale valore entra?
  • Quale valore esce?
  • Cosa è cambiato rispetto a quando funzionava?
  • Il problema è sempre riproducibile?
  • Posso riprodurlo in un ambiente più semplice?

Sembrano domande elementari.

Ed è proprio questo il punto.

Quando hai costruito sopra il problema dieci ipotesi, tornare a ciò che puoi realmente osservare permette di eliminare il rumore.

Non avere paura di ricominciare

A volte dopo due ore di debugging ti rendi conto di essere completamente fuori strada.

Hai seguito una pista sbagliata.

Hai analizzato il componente sbagliato.

Hai dato per scontato qualcosa che non era vero.

In quel momento la cosa peggiore che puoi fare è continuare su quella strada solo perché ci hai già investito del tempo.

Il tempo già speso non rende corretta un’ipotesi.

Se necessario, fai un passo indietro.

Riproduci nuovamente il problema.

Riscrivi quello che sai.

Elimina le supposizioni.

Riparti dai fatti.

Non è tempo perso.

È spesso il modo più veloce per uscire da un vicolo cieco.

La vera abilità non è risolvere bug impossibili

Con l’esperienza non smetti di incontrare bug strani, ne incontri semplicemente di più. La differenza è che impari a reagire diversamente. Non pensi immediatamente:

“Come posso aggiustarlo?”

Inizi a pensare:

“Quale comportamento mi aspettavo?”

“Quale comportamento sto osservando?”

“Qual è la differenza tra i due?”

“Quale delle mie supposizioni non è stata ancora verificata?”

Da quel momento il bug smette di essere qualcosa di misterioso e diventa un problema che puoi scomporre. Ogni informazione che raccogli restringe lo spazio delle possibili cause.

Il bug impossibile spesso non è impossibile

Un bug sembra impossibile quando la nostra spiegazione del sistema non riesce a giustificare ciò che stiamo osservando, ma il programma non deve rispettare la nostra spiegazione. Deve rispettare il codice, i dati, l’ambiente e le regole con cui sta realmente funzionando. Quando qualcosa non torna, quindi, non bisogna cercare immediatamente di convincere il programma a comportarsi come ci aspettiamo. Bisogna capire dove la nostra comprensione non coincide con la realtà. È questo il passaggio mentale più importante.

Un developer inesperto tende a combattere contro il bug. Un developer che sta acquisendo esperienza cerca di interrogarlo. Non cerca subito una risposta, ma cerca informazioni. Non cambia tutto, ma cambia una cosa e osserva. Non difende la propria teoria a spada tratta, ma cerca di smentirla. E soprattutto, non considera risolto un problema soltanto perché il codice ha smesso momentaneamente di comportarsi male.

Perché davanti a un bug che sembra impossibile, la domanda più utile non è:

“Come faccio a farlo funzionare?”

È:

“Che cosa sta realmente succedendo?”