“Kan je me even helpen bij het uitlezen van een cookie?” was een vraag die ik op een dinsdagochtend kreeg. De persoon achter de vraag was bezig om met Robot framework wat testen te automatiseren, maar er kwam een foutmelding bij het submitten van een form. Op dat moment had ik geen tijd, maar aan het einde van de dag vroeg ik hem of het nog gelukt was. Hij zat nog steeds met hetzelfde probleem en ik besloot mee te kijken. Mijn eerste vraag was: “Weet je eigenlijk wel zeker dat het probleem met de cookie te maken heeft?” Na een onbevredigend antwoord heb ik even handmatig de waarde uit het cookie gekopieerd, in de test gezet en het form geprobeerd te submitten. Ik zei hierbij hardop: “Dus als het aan de cookie lag, dan zou de fout nu moeten verdwijnen”. Toen bleek dat het nog steeds dezelfde fout gaf. Na de foutmelding te hebben gelezen, bleek dat hij één veld vergeten was in te vullen. De cookie had er niets mee te maken.
Naam auteur onbekend bij de redactie.
Ik heb een groot deel van mijn carrière bugs opgelost en vind het vaak ook een leuke puzzel om te ontdekken waar de fout is ontstaan. Ik merk ook dat ik bijna iedere keer nadat ik een probleem heb opgelost, het traject wat ik heb doorlopen probeer te analyseren. Ik probeer te bedenken of ik ergens een fout heb gemaakt. Of hoe ik deze bug sneller of efficiënter had kunnen oplossen. Na meerdere voorbeelden, zoals hierboven, te hebben meegemaakt leek het een goed moment om na te gaan wat ik inmiddels allemaal heb geleerd om snel een goede root-cause analyse uit te voeren en problemen goed op te lossen. Mijn conclusies wil ik graag met jullie delen aan de hand van enkele technieken en tips.
Laten we beginnen met de belangrijkste tip. Dit is wat naar mijn mening het meest fout gaat in de praktijk, namelijk het niet controleren van aannames. Het doen van aannames is een heel natuurlijk iets. We doen in het leven continu aannames die we niet controleren. Wanneer je bijvoorbeeld op een stoel gaat zitten, dan ga je niet controleren of alle poten wel in orde zijn. Zo ook tijdens het debuggen. Ergens in het proces krijg je op een gegeven moment een idee over wat er fout zou kunnen gaan.
In het voorbeeld van de intro zag de persoon dat het submitten van het formulier niet lukte. Hij zag als verschil dat er op de website een cookie werd meegestuurd wat hij niet had met het robot-framework. Hierdoor creëerde hij de aanname dat het waarschijnlijk aan de missende cookie lag. Onbewust ontstond hier een tunnelvisie mee. Daarbij had hij ook nog de pech dat dit (niet bestaande probleem) erg lastig was op te lossen. Op die manier is bijna een dag verloren gegaan. De belangrijkste les die we hieruit kunnen trekken, is dat het erg belangrijk is om zeker te weten dat je wel het juiste probleem aan het oplossen bent. Probeer altijd, als je een aanname gedaan hebt, een manier te vinden om te controleren of de aanname juist is. Het hoeft dus geen oplossing te zijn. Het mag zo hacky zijn als het maar kan. Het is alleen maar om vast te stellen of de aanname correct is. Zodra blijkt dat de aanname correct is, kan je de hack weer verwijderen en tijd besteden aan het structureel oplossen van het probleem.
Een andere truc is het verwachte resultaat hardop zeggen. Mensen willen altijd heel graag gelijk hebben en onze hersenen zijn best goed geworden in het bijstellen van onze verwachtingen na het zien van resultaat. Hierdoor moest je vroeger tijdens scheikunde ook altijd je hypothese opschrijven, zodat je daar later niet meer van kon afwijken als je ineens iets vreemds observeerde na het experiment.
Door het verwachte resultaat hardop uit te spreken, gebeurt iets soortgelijks. Het statement wordt meer waar. Als het verwachte resultaat vervolgens afwijkt van het echte resultaat, dan kan je er niet meer onderuit en word je geforceerd hierover na te denken. Was je originele hypothese wellicht niet correct (is de nieuwe uitkomst toch logisch) of gaat wellicht toch iets anders fout dan je aanvankelijk had verwacht? Het hardop uitspreken hiervan helpt hierbij.
Het komt ook voor dat andere mensen je willen helpen met debuggen als ze een bug zijn tegengekomen. Dit kunnen mensen van buiten je project zijn of bijvoorbeeld een tester met een beetje technische kennis. Ze komen een bug tegen tijdens hun werk en vervolgens proberen ze zelf de oorzaak te bedenken. Dit is vaak erg goed bedoeld. Echter, doordat deze mensen vaak weinig relevante kennis hebben hoe de code werkt, doen ze veel aannames. En deze kunnen gevaarlijk zijn, omdat ze jou op het verkeerde spoor kunnen zetten. Wees hiervan bewust bij het krijgen van een stukje logging of bij het krijgen van aanwijzingen over in welk component het probleem moet zitten. Deze aannames hoeven dus niet gebaseerd te zijn op voor het probleem relevante feiten. Het is verstandig om de bug samen met deze persoon te reproduceren en vervolgens je eigen onderzoek te doen.
Logging is een belangrijk hulpmiddel. Het is echter zaak om dit hulpmiddel zo goed mogelijk in te zetten. Zo is het belangrijk dat je jezelf niet blind staart op de foutmelding die gerapporteerd is. Hoewel het op die plek fout gaat, betekent dit niet dat de fout ook daadwerkelijk op die plek is veroorzaakt. Een foutmelding bij het opstarten van de applicatie zou namelijk best de oorzaak kunnen zijn van de fout die pas veel later is opgetreden. Daarom is ook aan te raden om, indien mogelijk, altijd te beginnen bij de eerste foutmelding die is opgetreden na het opstarten van de applicatie. Werk vanuit daar verder naar beneden. Dit hoeven niet persé stack-traces te zijn. Het zou ook kunnen dat wat op ERROR of WARN level is gelogd, interessant is.
Als je wel een stack-trace hebt, dan is het correct lezen hiervan ook belangrijk. Een stack-trace bestaat vaak uit meerdere ‘caused-by’-statements. Meestal (afhankelijk van de logging configuratie) is de onderste ‘caused-by’ waar het echte probleem zich bevindt. Alles erboven is de route die de fout heeft gemaakt. In deze route kan meer informatie zitten over wat is fout gegaan, maar er verdwijnt ook veel informatie over de originele fout. Probeer dus eerst onderaan te zien wat de fout daadwerkelijk is en daarna naar boven te werken om te kijken hoe deze precies is ontstaan. Hierbij kan je over het algemeen regels negeren die met een andere package naam dan de package naam van je applicatie beginnen. De fout zit namelijk meestal niet in de frameworks die de rest van de wereld ook gebruiken. De kans is groter dat jouw code fout is. Begin bij het zoeken van de bovenste regel in de stack-trace die start met je eigen package. Samenvattend: eerste foutmelding in de log, laatste ‘caused-by’, let op je eigen packages.
Je hebt helaas niet altijd de luxe van een stack-trace. Soms bestaat een fout in het gedrag van je applicatie. Er is bijvoorbeeld rare data te zien in de frontend. Dan is het zaak om uit te vinden waar het probleem zit. De meest efficiënte manier om dit te doen is door het probleemgebied zoveel mogelijk te verkleinen. De truc is door iedere keer te kijken waar het nog wel goed gaat of tot waar het nog steeds fout gaat. In het voorbeeld van de rare data in de frontend is het verstandig om de browser tooling te gebruiken om te kijken welke data de frontend krijgt. Onderschep de JSON die de frontend krijgt en controleer of de data hierin correct is. Is de data hier correct, dan moet de fout in de frontend zitten. Is de data fout, dan ligt het probleem zeer waarschijnlijk in de backend. Probeer dit principe door te zetten, totdat je het component hebt gevonden waar de fout zit. Indien je weet in welk component de fout zit, dan kan je eventueel met je debugger aan de slag.
Debuggers zijn tegenwoordig erg krachtig. Door de features goed te gebruiken, kan je het debug proces aanzienlijk versnellen. Gebruik bijvoorbeeld niet alleen maar de standaard breakpoints. Kijk ook naar opties als ‘conditional breakpoints’ (if condities in breakpoints), of ‘break on exception’ (break automatisch op bijvoorbeeld een null-pointer exception). Dit voorkomt wellicht erg veel doorstappen met de debugger.
Verder is het ook raadzaam om te beginnen met het zetten van een breakpoint op de plaats waar de fout is opgetreden. De stack-trace kan je hierbij helpen. Dan kan vanaf hier -door middel van de bijbehorende stack- terug gekeken worden hoe de fout is ontstaan. Dit werkt vaak beter dan een breakpoint op het entry-point in de applicatie te zetten om vervolgens door te steppen, totdat je op de plaats komt waar de fout is opgetreden. Allemaal in de hoop dat je op het oog kan zien wat fout gegaan is.
Een ander krachtig hulpmiddel is het unit-test framework. Door het hebben van een unit test die de bug triggert, creëer je een erg korte feedback-loop. Dit kan je helpen met het uitzoeken waar het probleem zich precies bevindt, omdat je er zo gemakkelijk iedere keer doorheen kan lopen. Deze korte feedback-loop zorgt ervoor dat je eventueel met wat trial-and-error-methodiek kan werken om uit te zoeken wat nou precies gebeurt. Daarnaast zorgt dit ervoor dat er ook een testcase is die garandeert dat de bug zich niet meer kan voordoen in de toekomst.
Je hebt nu alle bovenstaande stappen doorlopen. Je hebt uitgezocht wat fout gaat en een test geschreven om de bug te triggeren. Nu is het tijd de bug op te lossen. Wat hierbij heel belangrijk is, is dat de bug op de juiste plaats wordt opgelost. Het is gemakkelijk om bijvoorbeeld bij een null-pointer een ‘if(property != null)’ check te plaatsen. Echter, je moet goed kijken of die null daadwerkelijk de oorzaak is of dat het niet een symptoom is van de echte oorzaak. Stel daarom ook jezelf de vraag: “Hoe kan het zijn dat de input van de functie op die plek een illegale waarde bevatte?”
Soms is de null-check correct. Dat betekent dat er ergens een validatie aan het begin van het proces miste. Probeer daarom ook zo vroeg mogelijk in het proces te voorkomen dat een mogelijk fout object wordt doorgestuurd. Dit voorkomt dat over de hele code symptoombestrijding in de vorm van bijvoorbeeld null-checks worden gedaan.
Voorkomen is altijd beter dan genezen. Er zijn veel technieken, tooling en frameworks die je kunnen helpen bij het voorkomen van bugs. Ik zal daar niet diep op ingaan in dit artikel, maar noemenswaardige dingen om naar te kijken zijn:
En verder zijn er nog enkele paradigma’s, zoals Immutability, totality, idempotent functions en pure functions, welke erg goed helpen in het voorkomen van bepaalde fouten.