Murphy’s wet stelt dat alles wat fout kan gaan uiteindelijk fout een keer zal gaan. Helaas is de programmeerwereld een van de plekken waar deze wet net iets te goed op toegepast kan worden. Bij het maken van een applicatie is de kans groot dat je bugs en andere problemen veroorzaakt en daarbij zijn fouten in JavaScript een veelvoorkomend probleem!

Het succes van een softwareproduct hangt af van hoe goed de makers deze problemen kunnen oplossen voordat ze gebruikers hier last van krijgen. En JavaScript is – van alle programmeertalen – berucht voor de manier waarop foutafhandeling is ingericht.

Als je een JavaScript applicatie bouwt, is de kans groot dat je op een of ander moment met data types in de war haalt. Mocht dit specifieke scenario je bespaard zijn gebleven, dan heb je vast wel eens een undefined voor een null ingeruild of een triple equals operator (===) vervangen door een double equals operator (==).

Het is dan ook menselijk om fouten te maken en het overkomt iedereen. En daarom laten we je alles zien wat je moet weten over hoe je met fouten omgaat in JavaScript.

Dit artikel leidt je door de basic fouten in JavaScript en legt de verschillende fouten uit die je kunt tegenkomen. Je leert vervolgens hoe je deze fouten kunt identificeren en oplossen. Er zijn ook een aantal tips om effectief om te gaan met fouten in productieomgevingen.

Laten we maar snel beginnen!

Bekijk onze videogids over het omgaan met JavaScript fouten

Wat zijn JavaScript fouten?

Fouten bij het programmeren verwijzen naar situaties waarin een programma niet functioneert zoals het zou moeten. Het kan voorkomen wanneer een programma niet weet hoe het de taak moet afhandelen, zoals wanneer het probeert een niet-bestaand bestand te openen of een webgebaseerd API endpoint probeert te bereiken terwijl er geen netwerkverbinding is.

Deze situaties zorgen ervoor dat het programma fouten naar de gebruiker stuurt en stelt dat het niet weet wat er nu moet gebeuren. Het programma verzamelt zoveel mogelijk informatie over de fout en meldt vervolgens dat het niet verder kan.

Intelligente developers proberen deze scenario’s voor te zijn en zich in te dekken, zodat de gebruiker niet zelf een technische foutmelding als “404” hoeft uit te vogelen. In plaats daarvan tonen ze een veel begrijpelijker bericht: “The page could not be found.”

Fouten in JavaScript zijn objecten die worden weergegeven wanneer er een programmeerfout optreedt. Deze objecten bevatten veel informatie over het type fout, de instructie die de fout heeft veroorzaakt en de stack trace op het moment dat de fout optrad. JavaScript stelt programmeurs ook in staat om custom errors (aangepaste fouten) te maken om extra informatie te verstrekken bij het oplossen van problemen.

Eigenschappen van een fout

Nu de definitie van een JavaScript fout duidelijk is, is het tijd om dieper in de materie te duiken.

Fouten in JavaScript hebben bepaalde standaard- en aangepaste eigenschappen die helpen de oorzaak en gevolgen van de fout te begrijpen. Fouten in JavaScript bevatten standaard drie eigenschappen:

  1. message: Een tekenreekswaarde die het foutbericht overbrengt
  2. name: Het type fout dat zich heeft voorgedaan (we zullen hier dieper op ingaan in de volgende sectie)
  3. stack: De stack trace van de uitgevoerd code toen de fout optrad.

Bovendien kunnen fouten ook eigenschappen bevatten zoals columnNumber, lineNumber, fileName, enz. om de fout beter te beschrijven. Deze eigenschappen zijn echter niet standaard en kunnen al dan niet aanwezig zijn in elk foutobject dat door jouw JavaScript applicatie wordt gegenereerd.

Stack trace begrijpen

Een stack trace is de lijst met method calls waarin een programma zich bevond toen een event, zoals een uitzondering of een waarschuwing, plaatsvond. Dit is hoe een voorbeeld stack trace, vergezeld door een uitzondering, eruitziet:

Voorbeeld van een stack trace.
Voorbeeld van een stack trace.

Zoals je kunt zien, begint het met het afdrukken van de foutnaam en het bericht, gevolgd door een lijst met methoden die werden gecallt. Elke method call vermeldt de locatie van de source code en de regel waarop deze werd gecallt. Je kunt deze gegevens gebruiken om door je codebase te navigeren en te identificeren welk stuk code de fout veroorzaakt.

Deze lijst met methoden is “stacked” gerangschikt. Het laat zien waar je uitzondering voor het eerst werd gegenereerd en hoe deze zich verspreidde via de stacked method calls. Als je een catch voor de uitzondering implementeert, kan deze zich niet door de stack verspreiden en je programma laten crashen. In sommige scenario’s wil je echter mogelijk fatale fouten onopgemerkt laten om het programma opzettelijk te laten crashen.

Fouten vs uitzonderingen

De meeste mensen beschouwen fouten en uitzonderingen meestal als hetzelfde. Het is echter essentieel om een klein maar fundamenteel verschil tussen de twee op te merken.

Laten we een kort voorbeeld nemen om dit beter te begrijpen. Dit is hoe je een fout in JavaScript kunt definiëren:

const wrongTypeError = TypeError("Wrong type found, expected character")

En zo wordt het wrongTypeError  object een uitzondering:

throw wrongTypeError

De meeste mensen hebben echter de neiging om de shorthand vorm te gebruiken die foutobjecten definieert terwijl ze worden gegenereerd:

throw TypeError("Wrong type found, expected character")

Dit is de standaardpraktijk. Het is echter een van de redenen waarom ontwikkelaars de neiging hebben om uitzonderingen en fouten door elkaar te halen. Daarom is het essentieel om de grondbeginselen te kennen, ook al gebruik je shorthands om je werk snel gedaan te krijgen.

Soorten fouten in JavaScript

Er is een reeks vooraf gedefinieerde fouttypen in JavaScript. Ze worden automatisch gekozen en gedefinieerd door de JavaScript runtime wanneer de programmeur niet expliciet fouten in de applicatie afhandelt.

In dit gedeelte word je door enkele van de meest voorkomende soorten fouten in JavaScript geleid en begrijp je wanneer en waarom ze optreden.

RangeError

Er wordt een RangeError gegenereerd wanneer een variabele wordt ingesteld met een waarde buiten het bereik van de “legal” waarden. Het komt meestal voor bij het doorgeven van een waarde als argument aan een functie, en de gegeven waarde ligt niet in het bereik van de parameters van de functie. Het kan soms lastig zijn om deze fout op te lossen wanneer je beschikt over slecht gedocumenteerde externe bibliotheken, omdat je het bereik van mogelijke waarden moet kennen voordat de argumenten de juiste waarde kunnen doorgeven.

Enkele veelvoorkomende scenario’s waarin RangeError optreedt, zijn:

  • Proberen een array van illegale lengtes te maken via de array-constructor.
  • Slechte waarden doorgeven aan numerieke methoden zoals toExponential(), toPrecision(), toFixed(), etc.
  • Ongeldige waarden doorgeven aan tekenreeksfuncties zoals normalize().

ReferenceError

Een ReferenceError treedt op wanneer er iets mis is met de reference van een variabele in je code. Mogelijk ben je vergeten een waarde voor de variabele te definiëren voordat je deze gebruikt, of probeer je een ontoegankelijke variabele in je code te gebruiken. Hoe dan ook, het doorlopen van de stack trace biedt voldoende informatie om de variabele referentie die de fout heeft te vinden en te repareren.

Enkele veelvoorkomende redenen waarom ReferenceErrors optreden zijn:

  • Een typefout maken in een variabelenaam.
  • Proberen toegang te krijgen tot variabelen met een blokbereik buiten hun bereik.
  • Verwijzen naar een globale variabele uit een externe bibliotheek (zoals $ uit jQuery) voordat deze is geladen.

SyntaxError

Deze fouten zijn een van de eenvoudigste om op te lossen, omdat ze wijzen op een fout in de syntaxis van de code. Omdat JavaScript een scripttaal is die wordt geïnterpreteerd in plaats van gecompileerd, worden deze gegenereerd wanneer de app het script uitvoert dat de fout bevat. In het geval van gecompileerde talen worden dergelijke fouten tijdens het compileren geïdentificeerd. De binaire bestanden van de app worden dus pas gemaakt als deze zijn opgelost.

Enkele veelvoorkomende redenen waarom SyntaxErrors kunnen optreden zijn:

  • Ontbrekende aanhalingstekens
  • Ontbrekende haakjes sluiten
  • Onjuiste uitlijning van accolades (curly breaks) of andere tekens

Het is een goede gewoonte om een linting tool in je IDE te gebruiken om dergelijke fouten voor je te identificeren voordat ze in de browser terechtkomen.

TypeError

TypeError is een van de meest voorkomende fouten in JavaScript apps. Deze fout wordt gemaakt wanneer een waarde niet van een bepaald verwacht type blijkt te zijn. Enkele veelvoorkomende gevallen waarin het optreedt, zijn:

  • Objecten callen die geen methoden zijn.
  • Poging om toegang te krijgen tot eigenschappen van null of undefined objecten
  • Een tekenreeks als een getal behandelen of omgekeerd

Er zijn veel meer mogelijkheden waar een TypeError kan optreden. We zullen later naar enkele bekende gevallen kijken en leren hoe we ze kunnen oplossen.

InternalError

Het type InternalError wordt gebruikt wanneer er een uitzondering optreedt in de JavaScript runtime engine. Het kan al dan niet wijzen op een probleem met je code.

Vaak komt InternalError slechts in twee scenario’s voor:

  • Wanneer een patch of een update van de JavaScript runtime een bug bevat die uitzonderingen genereert (dit gebeurt zeer zelden)
  • Wanneer je code entiteiten bevat die te groot zijn voor de JavaScript engine (bijv. te veel switch cases, te grote array initializers, te veel recursie)

De meest geschikte aanpak om deze fout op te lossen, is door de oorzaak te identificeren via het foutbericht en je app logica, indien mogelijk, te herstructureren om de plotselinge piek van de werkbelasting op de JavaScript engine te elimineren.

URIError

URIError treedt op wanneer een algemene URI verwerkingsfunctie zoals decodeURIComponent ongeldig wordt gebruikt. Het geeft meestal aan dat de parameter die aan de method call is doorgegeven, niet voldeed aan de URI standaarden en dus niet correct door de methode werd geparsed.

Het diagnosticeren van deze fouten is meestal eenvoudig, omdat je alleen de argumenten op malformation hoeft te onderzoeken.

EvalError

Een EvalError treedt op wanneer er een fout optreedt bij een functie call eval() De functie eval()wordt gebruikt om JavaScript code uit te voeren die is opgeslagen in tekenreeksen. Omdat het gebruik van de eval() functie echter ten zeerste wordt afgeraden vanwege beveiligingsproblemen en de huidige ECMAScript specificaties de EvalError klasse niet meer genereren, bestaat dit fouttype eenvoudigweg om achterwaartse compatibiliteit met oudere JavaScript code te behouden.

Als je met een oudere versie van JavaScript werkt, kun je deze fout tegenkomen. In ieder geval is het het beste om de code die wordt uitgevoerd in de functie call eval()te onderzoeken op eventuele uitzonderingen.

Custom error types maken

Hoewel JavaScript een adequate lijst van klassen van error types (fouttype) biedt voor de meeste scenario’s, kun je altijd een nieuwe error type maken als de lijst niet aan jouw vereisten voldoet. De basis van deze flexibiliteit ligt in het feit dat je met JavaScript alles letterlijk kunt genereren met de  throw opdracht.

Dus technisch gezien zijn deze verklaringen volledig geldig:

throw 8
throw "An error occurred"

Het genereren van een primitief gegevenstype geeft echter geen details over de fout, zoals het type, de naam of de bijbehorende stack trace. Om dit op te lossen en het foutafhandelingsproces te standaardiseren, is de Error klasse geleverd. Het wordt ook afgeraden om primitieve gegevenstypen te gebruiken terwijl uitzonderingen worden gegenereerd.

Je kunt de Error  klasse uitbreiden om je aangepaste foutklasse te maken. Hier is een eenvoudig voorbeeld van hoe je dit kunt doen:

class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

En je kunt het als volgt gebruiken:

throw ValidationError("Property not found: name")

En je kunt het vervolgens identificeren met behulp van het trefwoord instanceof:

try {
    validateForm() // code that throws a ValidationError
} catch (e) {
    if (e instanceof ValidationError)
    // do something
    else
    // do something else
}

Top 10 meest voorkomende fouten in JavaScript

Nu je de veelvoorkomende fouttypen begrijpt en weet hoe je je aangepaste fouten kunt maken, is het tijd om een paar van de meest voorkomende fouten te bekijken die je tegenkomt bij het schrijven van JavaScript code.

1. Uncaught RangeError

Deze fout treedt op in Google Chrome in een aantal verschillende scenario’s. Ten eerste kan het gebeuren als je een recursieve functie aanroept en deze niet wordt beëindigd. Je kunt dit zelf bekijken in de Chrome Developer Console:

RangeError voorbeeld met een recursieve functie call.
RangeError voorbeeld met een recursieve functie call.

Dus om een dergelijke fout op te lossen, moet je ervoor zorgen dat je de border cases van je recursieve functie correct definieert. Een andere reden waarom deze fout optreedt, is als je een waarde hebt doorgegeven die buiten het bereik van een functieparameter ligt. Hier is een voorbeeld:

RangeError voorbeeld met toExponential() call.
RangeError voorbeeld met toExponential() call.

De foutmelding geeft meestal aan wat er mis is met je code. Zodra je de wijzigingen aanbrengt, wordt dit opgelost.

Output voor de functie call toExponential().
Output voor de functie call toExponential().

2. Uncaught TypeError: Cannot set property

Deze fout treedt op wanneer je een eigenschap instelt op een ongedefinieerde referentie. Je kunt het probleem reproduceren met deze code:

var list
list.count = 0

Dit is de output die je ontvangt:

TypeError voorbeeld.
TypeError voorbeeld.

Om deze fout op te lossen, initialiseer je de referentie met een waarde voordat je de eigenschappen opent. Zo ziet het eruit als het is opgelost:

Hoe TypeError op te lossen.
Hoe TypeError op te lossen.

3. Uncaught TypeError: Cannot read property

Dit is een van de meest voorkomende fouten in JavaScript. Deze fout treedt op wanneer je probeert een eigenschap te lezen of een functie te callen voor een ongedefinieerd object. Je kunt het heel gemakkelijk reproduceren door de volgende code uit te voeren in een Chrome Developer console:

var func
func.call()

Hier is de output:

TypeError voorbeeld met ongedefinieerde functie.
TypeError voorbeeld met ongedefinieerde functie.

Een ongedefinieerd object is een van de vele mogelijke oorzaken van deze fout. Een andere prominente oorzaak van dit probleem kan een onjuiste initialisatie van de status zijn tijdens het weergeven van de gebruikersinterface. Hier is een praktijkvoorbeeld van een React applicatie:

import React, { useState, useEffect } from "react";

const CardsList = () => {

    const [state, setState] = useState();

    useEffect(() => {
        setTimeout(() => setState({ items: ["Card 1", "Card 2"] }), 2000);
    }, []);

    return (
        <>
            {state.items.map((item) => (
                <li key={item}>{item}</li>
            ))}
        </>
    );
};

export default CardsList;

De app start met een lege instance container en wordt na een vertraging van 2 seconden voorzien van enkele items. De vertraging wordt ingesteld om een netwerk call te imiteren. Zelfs als je netwerk supersnel is, heb je nog steeds te maken met een kleine vertraging waardoor de component minstens één keer wordt gerenderd. Als je deze app probeert uit te voeren, ontvang je de volgende foutmelding:

TypeError stack trace in a browser.
TypeError stack trace in a browser.

Dit komt omdat op het moment van renderen de statuscontainer niet gedefinieerd is; er staan dus geen property items op. Het oplossen van deze fout is eenvoudig. Je hoeft alleen een initiële standaardwaarde op te geven voor de statuscontainer.

// ...
const [state, setState] = useState({items: []});
// ...

Nu, na de ingestelde vertraging, toont je app een vergelijkbare output:

Code output.
Code output.

De exacte oplossing in je code kan anders zijn, maar de essentie hier is om je variabelen altijd correct te initialiseren voordat je ze gebruikt.

4. TypeError: ‘undefined’ is not an object

Deze fout treedt op in Safari wanneer je toegang probeert te krijgen tot de eigenschappen van, of een methode callt op een ongedefinieerd object. Je kunt dezelfde code van boven uitvoeren om de fout zelf te reproduceren.

TypeError voorbeeld met ongedefinieerde functie.
TypeError voorbeeld met ongedefinieerde functie.

De oplossing voor deze fout is dan ook hetzelfde – zorg ervoor dat je je variabelen correct hebt geïnitialiseerd en dat ze niet ongedefinieerd zijn wanneer een eigenschap of methode wordt geopend.

5. TypeError: null is not an object

Deze is, nogmaals, vergelijkbaar met de vorige fout. Het komt voor in Safari, en het enige verschil tussen de twee fouten is dat deze wordt gegenereerd wanneer het object waarvan de eigenschap of methode wordt benaderd, null is in plaats van undefined. Je kunt dit reproduceren door het volgende stuk code uit te voeren:

var func = null

func.call()

Dit is de output die je ontvangt:

TypeError voorbeeld met null functie.
TypeError voorbeeld met null functie.

Omdat null een waarde is die expliciet is ingesteld op een variabele en niet automatisch wordt toegewezen door JavaScript. Deze fout kan alleen optreden als je een variabele te openen die je zelf op null hebt ingesteld. Je moet dus je code opnieuw bekijken en controleren of de logica die je hebt geschreven correct is of niet.

6. TypeError: Cannot read property ‘length’

Deze fout treedt op in Chrome wanneer je de lengte van een null of undefined object probeert te lezen. De oorzaak van dit probleem is vergelijkbaar met de vorige problemen, maar het komt vrij vaak voor bij het afhandelen van lijsten; daarom verdient het een speciale vermelding. Zo kun je het probleem reproduceren:

Voorbeeld TypeError met een ongedefinieerd object.
Voorbeeld TypeError met een ongedefinieerd object.

In de nieuwere versies van Chrome wordt deze fout echter gerapporteerd als Uncaught TypeError: Cannot read properties of undefined. Zo ziet het er nu uit:

TypeError voorbeeld met een ongedefinieerd object in nieuwere Chrome versies.
TypeError voorbeeld met een ongedefinieerd object in nieuwere Chrome versies.

De oplossing is opnieuw om ervoor te zorgen dat het object waarvan je de lengte probeert te openen, bestaat en niet is ingesteld op null.

7. TypeError: ‘undefined’ is not a function

Deze fout treedt op wanneer je een methode probeert te callen die niet in je script voorkomt, of wanneer deze wel bestaat, maar waarnaar niet kan worden verwezen in de callende context. Deze fout treedt meestal op in Google Chrome en je kunt deze oplossen door de coderegel te controleren die de fout genereert. Als je een typefout vindt, corrigeer je deze en controleer je of je probleem hiermee is opgelost.

Als jehet zelfverwijzende sleutelwoord this in je code hebt gebruikt, kan deze fout optreden als this niet op de juiste manier aan je context is gebonden. Beschouw de volgende code:

function showAlert() {
    alert("message here")
}

document.addEventListener("click", () => {
    this.showAlert();
})

Als je de bovenstaande code uitvoert, wordt de fout gegenereerd die we hebben besproken. Het gebeurt omdat de anonieme functie die is doorgegeven als de event listener wordt uitgevoerd in de context van het document.

Daarentegen is de functie showAlert gedefinieerd in de context van de border.

Om dit op te lossen, moet je de juiste verwijzing naar de functie doorgeven door deze te binden met de bind() methode:

document.addEventListener("click", this.showAlert.bind(this))

8. ReferenceError: event is not defined

Deze fout treedt op wanneer je probeert toegang te krijgen tot een verwijzing die niet is gedefinieerd in het gecallde bereik. Dit gebeurt meestal bij het afhandelen van events, omdat ze je vaak een referentie geven met de naam event in de callback functie. Deze fout kan optreden als je vergeet het event argument in de parameters van je functie te definiëren of als je deze verkeerd spelt.

Deze fout treedt mogelijk niet op in Internet Explorer of Google Chrome (omdat IE een globale event variabele biedt en Chrome de event variabele automatisch aan de handler koppelt), maar het kan voorkomen in Firefox. Het is dus raadzaam om op zulke kleine foutjes te letten.

9. TypeError: Assignment to constant variable

Dit is een fout die voortkomt uit onvoorzichtigheid. Als je een nieuwe waarde aan een constante variabele probeert toe te wijzen, krijg je een dergelijk resultaat:

Voorbeeld TypeError met constante objecttoewijzing.
Voorbeeld TypeError met constante objecttoewijzing.

Hoewel het nu gemakkelijk lijkt op te lossen, stel je honderden van dergelijke variabele declaraties voor en een ervan is ten onrechte gedefinieerd als const in plaats van let! In tegenstelling tot andere scripttalen zoals PHP, is er een minimaal verschil tussen de stijl van het declareren van constanten en variabelen in JavaScript. Daarom is het raadzaam om eerst je declaraties te controleren wanneer je met deze fout wordt geconfronteerd. Je kunt deze fout ook tegenkomen als je vergeet dat de genoemde verwijzing een constante is en deze als variabele gebruikt. Dit duidt op onzorgvuldigheid of een fout in de logica van je app. Zorg ervoor dat je dit controleert wanneer je dit probleem probeert op te lossen.

10. (unknown): Script error

Er treedt een scriptfout op wanneer een extern script een fout naar je browser stuurt. Deze fout wordt gevolgd door (unknown) omdat het externe script tot een ander domein dan je app behoort. De browser verbergt andere details om te voorkomen dat gevoelige informatie uit het externe script lekt.

Je kunt deze fout niet oplossen zonder de volledige details te kennen. Je kunt als volgt meer informatie over de fout krijgen:

  1. Voeg het crossorigin kenmerk toe aan de scripttag.
  2. Stel de juiste Access-Control-Allow-Origin header in op de server waarop het script wordt gehost
  3. [Optioneel] Als je geen toegang hebt tot de server die het script host, kun je overwegen een proxy te gebruiken om je verzoek door te sturen naar de server en terug naar de client met de juiste headers.

Zodra je toegang hebt tot de details van de fout, kun je het probleem oplossen, waarschijnlijk met de externe bibliotheek of het netwerk.

Zo identificeer en voorkom je fouten in JavaScript

Hoewel de hierboven besproken fouten de meest voorkomende en frequente zijn in JavaScript die je tegen zult komen, kan vertrouwen op een paar voorbeelden nooit genoeg zijn. Het is van vitaal belang om te begrijpen hoe je elk type fout in een JavaScript applicatie kunt detecteren en voorkomen tijdens het ontwikkelen ervan. Dit is hoe je fouten in JavaScript kunt oplossen.

Handmatig fouten genereren en vangen

De meest fundamentele manier om fouten af te handelen die handmatig of tijdens runtime zijn gegenereerd, is door ze op te vangen, te catchen. Net als de meeste andere talen biedt JavaScript een reeks trefwoorden om fouten op te lossen. Het is essentieel om elk van hen grondig te kennen voordat je begint met het afhandelen van fouten in je JavaScript app.

throw

Het eerste en meest elementaire sleutelwoord van de set is throw. Zoals duidelijk is, wordt het throw sleutelwoord gebruikt om fouten te genereren om handmatig uitzonderingen te maken in de JavaScript runtime. We hebben dit al eerder in het stuk besproken, en hier is de kern van de betekenis van dit sleutelwoord:

  • Je kunt throw overal voor gebruiken, inclusief getallen, tekenreeksen en Error objecten.
  • Het is echter niet aan te raden om throw voor primitieve gegevenstypen zoals tekenreeksen en getallen te gebruiken, omdat ze geen foutopsporingsinformatie over de fouten bevatten.
  • Voorbeeld: throw TypeError("Please provide a string")

try

Het try sleutelwoord wordt gebruikt om aan te geven dat een codeblok een uitzondering kan veroorzaken. De syntaxis is:

try {
    // error-prone code here
}

Het is belangrijk op te merken dat een catch blok altijd het try blok moet volgen om fouten effectief af te handelen.

catch

Het catch sleutelwoord wordt gebruikt om een catch blok te maken. Dit codeblok is verantwoordelijk voor het afhandelen van de fouten die het trailing try blok opvangt. De syntaxis is:

catch (exception) {
    // code to handle the exception here
}

In tegenstelling tot C++ of Java kun je in JavaScript niet meerdere catch blokken aan een try blok toevoegen. Dit betekent dat je dit niet kunt doen:

try {
    // business logic code
} catch (exception) {
    // error handling code
}

In tegenstelling tot C++ of Java kun je in JavaScript niet meerdere catch blokken aan een try blok toevoegen. Dit betekent dat je dit niet kunt doen:

try {
    // business logic code
} catch (exception) {
    if (exception instanceof TypeError) {
        // do something
    }
} catch (exception) {
    if (exception instanceof RangeError) {
    // do something
    }
}

In plaats daarvan kun je een if...else instructie of een switch case instructie in het enkele catch blok gebruiken om alle mogelijke foutgevallen af te handelen. Het zou er zo uitzien:

try {
    // business logic code
} catch (exception) {
    if (exception instanceof TypeError) {
        // do something
    } else if (exception instanceof RangeError) {
        // do something else
    }
}

finally

Het finally sleutelwoord wordt gebruikt om een codeblok te definiëren dat wordt uitgevoerd nadat een fout is afgehandeld. Dit blok wordt uitgevoerd na de try en de catch blokken.

Ook wordt het definitieve blok uitgevoerd, ongeacht het resultaat van de andere twee blokken. Dit betekent dat zelfs als het catch blok de fout niet volledig kan verwerken of als er een fout in het catch blok wordt gegenereerd, de interpreter de code in het laatste blok zal uitvoeren voordat het programma crasht.

Om als geldig te worden beschouwd, moet het try blok in JavaScript worden gevolgd door een catch of een finally blok. Zonder een van deze zal de interpreter een SyntaxError opwerpen. Zorg er daarom voor dat je je try blokken met ten minste een van beide volgt bij het afhandelen van fouten.

Globaal fouten afhandelen met de onerror() methode

De onerror() methode is beschikbaar voor alle HTML elementen om eventuele fouten af te handelen. Als een img tag bijvoorbeeld de afbeelding waarvan de URL is opgegeven niet kan vinden, activeert deze zijn onerror methode zodat de gebruiker de fout kan afhandelen.

Normaal gesproken zou je een andere afbeeldings-URL opgeven in de onerror call waarop de img tag kan terugvallen. Zo doe je dat via JavaScript:

const image = document.querySelector("img")

image.onerror = (event) => {
    console.log("Error occurred: " + event)
}

Je kunt deze functie echter gebruiken om een globaal foutafhandelingsmechanisme voor je app te maken. Dit is hoe je het kunt doen:

window.onerror = (event) => {
    console.log("Error occurred: " + event)
}

Met deze eventhandler kun je de meerdere try...catch blokken die in je code rondslingeren verwijderen en de foutafhandeling van je app centraliseren, vergelijkbaar met event handling. Je kunt meerdere error handlers aan het venster koppelen om het Single Responsibility Principle van de SOLID ontwerpprincipes te behouden. De interpreter doorloopt alle handlers totdat hij de juiste bereikt.

Pass Errors via Callbacks

Hoewel eenvoudige en lineaire functies ervoor zorgen dat foutafhandeling eenvoudig blijft, kunnen callbacks de zaak bemoeilijken.

Kijk eens naar het volgende stukje code:

const calculateCube = (number, callback) => {
    setTimeout(() => {
        const cube = number * number * number
        callback(cube)
    }, 1000)
}

const callback = result => console.log(result)

calculateCube(4, callback)

De bovenstaande functie demonstreert een asynchrone condition waarin een functie enige tijd nodig heeft om bewerkingen te verwerken en het resultaat later retourneert met behulp van een callback.

Als je een tekenreeks probeert in te voeren in plaats van 4 in de functieaanroep, krijg je NaN als resultaat.

Hier moet goed mee omgegaan worden. Dit is hoe:

const calculateCube = (number, callback) => {

    setTimeout(() => {
        if (typeof number !== "number")
            throw new Error("Numeric argument is expected")

        const cube = number * number * number
        callback(cube)
    }, 1000)
}

const callback = result => console.log(result)

try {
    calculateCube(4, callback)
} catch (e) { console.log(e) }

Dit zou het probleem idealiter moeten oplossen. Als je echter probeert een tekenreeks door te geven aan de functieaanroep, ontvang je dit:

Foutvoorbeeld met het verkeerde argument.
Foutvoorbeeld met het verkeerde argument.

Ook al heb je een try-catch blok geïmplementeerd terwijl je de functie aanroept, het zegt nog steeds dat de fout niet is gevonden. De fout wordt gegenereerd nadat het catch blok is uitgevoerd vanwege de time-outvertraging.

Dit kan snel gebeuren bij netwerk calls, waar onverwachte vertragingen insluipen. Je moet dergelijke gevallen voor zijn tijdens het ontwikkelen van je app.

Zo handel je fouten in callbacks op de juiste manier af:

const calculateCube = (number, callback) => {

    setTimeout(() => {
        if (typeof number !== "number") {
            callback(new TypeError("Numeric argument is expected"))
            return
        }
        const cube = number * number * number
        callback(null, cube)
    }, 2000)
}

const callback = (error, result) => {
    if (error !== null) {
        console.log(error)
        return
    }
    console.log(result)
}

try {
    calculateCube('hey', callback)
} catch (e) {
    console.log(e)
}

Nu zal de output op de console zijn:

TypeError voorbeeld met ongeldig argument.
TypeError voorbeeld met ongeldig argument.

Dit geeft aan dat de fout op de juiste manier is afgehandeld.

Omgaan met fouten in promises

De meeste mensen geven de voorkeur aan promises voor het afhandelen van asynchrone activiteiten. Promises hebben nog een ander voordeel: een afgewezen promise beëindigt je script niet. Je moet echter nog steeds een catch blok implementeren om fouten in promises af ​​te handelen. Laten we, om dit beter te begrijpen, de calculateCube()functie herschrijven met promises:

const delay = ms => new Promise(res => setTimeout(res, ms));

const calculateCube = async (number) => {
    if (typeof number !== "number")
        throw Error("Numeric argument is expected")
    await delay(5000)
    const cube = number * number * number
    return cube
}

try {
    calculateCube(4).then(r => console.log(r))
} catch (e) { console.log(e) }

De time-out van de vorige code is geïsoleerd in de delay voor beter begrip. Als je een tekenreeks probeert in te voeren in plaats van 4, zal de output die je krijgt er ongeveer zo uitzien:

TypeError voorbeeld met een ongeldig argument in Promise.
TypeError voorbeeld met een ongeldig argument in Promise.

Nogmaals, dit komt doordat de Promise de fout genereert nadat al het andere de uitvoering heeft voltooid. De oplossing voor dit probleem is eenvoudig. Voeg eenvoudig een catch() aanroep toe aan de promise chain, als volgt:

calculateCube("hey")
.then(r => console.log(r))
.catch(e => console.log(e))

Nu zal de output zijn:

TypeError voorbeeld afgehandeld met ongeldig argument.
TypeError voorbeeld afgehandeld met ongeldig argument.

Je kunt zien hoe gemakkelijk het is om fouten met promises af te handelen. Bovendien kun je een blok finally() en de promise call koppelen om code toe te voegen die wordt uitgevoerd nadat de foutafhandeling is voltooid.

Als alternatief kun je ook fouten in promises afhandelen met behulp van de traditionele try-catch-finally techniek. Zo ziet je promise call er in dat geval uit:

try {
    let result = await calculateCube("hey")
    console.log(result)
} catch (e) {
    console.log(e)
} finally {
    console.log('Finally executed")
}

Dit werkt echter alleen binnen een asynchrone functie. Daarom is de meest geprefereerde manier om fouten in beloften aan te pakken, de catch en finally aan de promise aanroep te hangen.

throw/catch vs onerror() vs callbacks vs promises: welke is het beste?

Met vier methoden tot je beschikking, moet je weten hoe je de meest geschikte in een bepaalde gebruikssituatie kunt kiezen. Zo kun je zelf beslissen:

throw/catch

Je zult deze methode het grootste deel van de tijd gebruiken. Zorg ervoor dat je voorwaarden voor alle mogelijke fouten in je catch blok implementeert, en vergeet niet om een definitief blok op te nemen als je na het try blok wat opruimingsroutines binnen je geheugen moet uitvoeren.

Te veel try/catch blokken kunnen je code echter moeilijk te onderhouden maken. Als je je in een dergelijke situatie bevindt, wil je misschien fouten afhandelen via de global handler of de promise methode.

Bij het kiezen tussen asynchrone try/catch blokken en promise’s catch(), is het raadzaam om de asynchrone try/catch blokken te gebruiken, omdat ze je code lineair en gemakkelijk te debuggen maken.

onerror()

Het is het beste om de onerror() methode te gebruiken als je weet dat je app veel fouten moet afhandelen, en ze kunnen flink verspreid zijn over de codebase. Met de onerror methode kun je fouten afhandelen alsof het simpelweg een ander event is die door je applicatie wordt afgehandeld. Je kunt meerdere error handlers definiëren en deze bij de eerste rendering aan het venster van je app koppelen.

Je moet echter ook onthouden dat de onerror() methode een onnodige uitdaging kan zijn om op te zetten in kleinere projecten met een kleinere foutenmarge. Als je zeker weet dat je app niet te veel fouten genereert, werkt de traditionele throw/catch methode het beste voor jou.

Callbacks en promises

Foutafhandeling bij callbacks en promises verschilt vanwege hun codeontwerp en -structuur. Als je echter tussen deze twee kiest voordat je je code hebt geschreven, kun je het beste met promises werken.

Dit komt omdat promises een ingebouwde constructie hebben voor het koppelen van een catch() en een finally() blok om fouten makkelijk af te handelen. Deze methode is eenvoudiger en schoner dan het definiëren van aanvullende argumenten/het hergebruiken van bestaande argumenten om fouten af te handelen.

Houd wijzigingen bij met Git opslagplaatsen

Veel fouten ontstaan vaak door handmatige fouten in de codebase. Tijdens het ontwikkelen of debuggen van je code, kan het zijn dat je onnodige wijzigingen aanbrengt die ertoe kunnen leiden dat er nieuwe fouten in je codebase verschijnen. Geautomatiseerd testen is een geweldige manier om je code na elke wijziging te tracken. Het kan je echter alleen vertellen of er iets mis is. Als je niet regelmatig een backup van je code maakt, verspil je tijd aan het repareren van een functie of een script dat voorheen prima werkte.

Dit is waar git een rol speelt. Met een juiste commit strategie kun je je git geschiedenis gebruiken als een backupsysteem om je code te bekijken terwijl deze zich tijdens de ontwikkeling heeft geëvolueerd. Je kunt makkelijk door je oudere commits bladeren en erachter komen dat de versie van de functie voorheen goed werkte, maar fouten genereert na een niet-gerelateerde wijziging.

Je kunt dan de oude code herstellen of de twee versies vergelijken om te bepalen wat er mis is gegaan. Moderne webontwikkelingstools zoals GitHub Desktop of GitKraken helpen je om deze veranderingen naast elkaar te visualiseren en de fouten snel te achterhalen.

Een gewoonte die je kan helpen minder fouten te maken, is het uitvoeren van code reviews wanneer je een belangrijke wijziging in je code aanbrengt. Als je in een team werkt, kun je een pull verzoek maken en een teamlid dit grondig laten beoordelen. Dit zal je helpen een tweede paar ogen te gebruiken om eventuele fouten op te sporen die je mogelijk zijn ontgaan.

Best practices voor het afhandelen van fouten in JavaScript

De bovengenoemde methoden zijn geschikt om je te helpen bij het ontwerpen van een solide aanpak voor foutafhandeling voor je volgende JavaScript applicatie. Het is echter het beste om een paar dingen in gedachten te houden terwijl je ze implementeert om het beste uit je foutbestendigheid te halen. Hier zijn enkele tips om je te helpen.

1. Gebruik custom errors bij het afhandelen van operationele uitzonderingen

We hebben aan het begin van deze handleiding custom errors geïntroduceerd om je een idee te geven van hoe je de foutafhandeling kunt aanpassen aan het unieke geval van jouw applicatie. Het is raadzaam om waar mogelijk custom errors fouten te gebruiken in plaats van de generieke Error klasse, omdat deze de callende omgeving meer contextuele informatie geeft over de fout.

Bovendien kun je met customers errors controleren hoe een fout wordt weergegeven in de aanroepende omgeving. Dit betekent dat je ervoor kunt kiezen om specifieke details te verbergen of aanvullende informatie over de fout weer te geven wanneer je maar wilt.

Je kunt zo ver gaan dat je de content van de fout opmaakt volgens jouw behoeften. Dit geeft je betere controle over hoe de fout wordt geïnterpreteerd en afgehandeld.

2. “Swallow” je uitzonderingen niet!

Zelfs de meest ervaren ontwikkelaars maken vaak een beginnersfout: ze gebruiken uitzonderingsniveaus diep in hun code.

Je kunt situaties tegenkomen waarin je een stukje code hebt dat optioneel is om uit te kunnen voeren. Als het werkt, geweldig; als dat niet het geval is, hoef je er niets aan te doen.

In deze gevallen is het vaak verleidelijk om deze code in een try blok te plaatsen en er een leeg catch blok aan te koppelen. Door dit te doen, laat je dat stukje code echter openstaan ​​om elke vorm van fout te veroorzaken en ermee weg te komen. Dit kan gevaarlijk worden als je een grote codebase hebt en veel voorbeelden van dergelijke slechte constructies voor foutbeheer.

De beste manier om met uitzonderingen om te gaan, is door een niveau te bepalen waarop ze allemaal worden gedeeld en ze te verhogen totdat ze daar zijn. Dit niveau kan een controller zijn (in een MVC architectuur app) of een middleware (in een traditionele servergerichte app).

Op deze manier kom je te weten waar je alle fouten in je app kunt vinden en kun je kiezen hoe je ze oplost, zelfs als dat betekent dat je er niets aan hoeft te doen.

3. Gebruik een gecentraliseerde strategie voor logboeken en foutwaarschuwingen

Het loggen van een fout is vaak een integraal onderdeel van de afhandeling ervan. Degenen die er niet in slagen om een gecentraliseerde strategie voor het loggen van fouten te ontwikkelen, kunnen waardevolle informatie over het gebruik van hun app mislopen.

De eventlogboeken van een app kunnen je helpen cruciale gegevens over fouten te achterhalen en deze snel te debuggen. Als je de juiste waarschuwingsmechanismen in je app hebt ingesteld, kun je weten wanneer er een fout optreedt in je app voordat deze een groot deel van je gebruikersbestand bereikt.

Het is raadzaam om een vooraf gebouwde logger te gebruiken of er een te maken die aan jouw behoeften voldoet. Je kunt deze logger configureren om fouten af te handelen op basis van hun niveaus (waarschuwing, foutopsporing, info, enz.), en sommige loggers gaan zelfs zo ver dat logbestanden onmiddellijk naar externe logservers worden verzonden. Op deze manier kun je zien hoe de logica van je applicatie presteert bij actieve gebruikers.

4. Breng gebruikers op de juiste manier op de hoogte van fouten

Een ander goed punt om in gedachten te houden bij het definiëren van je foutafhandelingsstrategie is om de gebruiker in gedachten te houden.

Alle fouten die de normale werking van je app verstoren, moeten een zichtbare waarschuwing aan de gebruiker geven om hen op de hoogte te stellen dat er iets mis is gegaan, zodat de gebruiker kan proberen een oplossing te vinden. Als je een snelle oplossing voor de fout weet, zoals het opnieuw proberen van een bewerking of uitloggen en opnieuw inloggen, moet je dit in de waarschuwing vermelden om de gebruikerservaring in realtime te verbeteren.

In het geval van fouten die geen interferentie veroorzaken met de dagelijkse gebruikerservaring, kun je overwegen de waarschuwing te onderdrukken en de fout op een externe server te loggen om later op te lossen.

5. Implementeer een middleware (Node.js)

De Node.js omgeving ondersteunt middlewares om functionaliteiten toe te voegen aan serverapplicaties. Je kunt deze feature gebruiken om een middleware voor foutafhandeling voor je server te maken.

Het belangrijkste voordeel van het gebruik van middleware is dat al je fouten centraal op één plek worden afgehandeld. Je kunt ervoor kiezen om deze configuratie eenvoudig in of uit te schakelen voor testdoeleinden.

Zo maak je een basic middleware:

const logError = err => {
    console.log("ERROR: " + String(err))
}

const errorLoggerMiddleware = (err, req, res, next) => {
    logError(err)
    next(err)
}

const returnErrorMiddleware = (err, req, res, next) => {
    res.status(err.statusCode || 500)
       .send(err.message)
}

module.exports = {
    logError,
    errorLoggerMiddleware,
    returnErrorMiddleware
}

Je kunt deze middleware dan als volgt in je app gebruiken:

const { errorLoggerMiddleware, returnErrorMiddleware } = require('./errorMiddleware')

app.use(errorLoggerMiddleware)

app.use(returnErrorMiddleware)

Je kunt nu aangepaste logica binnen de middleware definiëren om fouten op de juiste manier af te handelen. Je hoeft zich geen zorgen meer te maken over het implementeren van individuele foutafhandelingsconstructies in je codebase.

6. Start je app opnieuw om programmeerfouten op te lossen (Node.js)

Wanneer Node.js apps programmeerfouten tegenkomen, hoeven ze niet per se een uitzondering te genereren en proberen ze de app te sluiten. Dergelijke fouten kunnen problemen omvatten die voortkomen uit programmeerfouten, zoals een hoog CPU verbruik, geheugen bloating of geheugenlekken. De beste manier om hiermee om te gaan, is door de app opnieuw te starten door hem te laten crashen via de Node.js clustermodus of een unieke tool zoals PM2. Dit kan ervoor zorgen dat de app niet crasht bij actie van de gebruiker, wat een vreselijke gebruikerservaring oplevert.

7. Catch alle niet-gecatchte uitzonderingen (Node.js)

Je kunt er nooit zeker van zijn dat je alle mogelijke fouten die in je app kunnen optreden, hebt opgevangen. Daarom is het essentieel om een fallback strategie te implementeren om alle niet-gecatchte uitzonderingen van je app op te vangen.

Zo kun je dat doen:

process.on('uncaughtException', error => {
    console.log("ERROR: " + String(error))
    // other handling mechanisms
})

Je kunt ook vaststellen of de opgetreden fout een standaarduitzondering is of een aangepaste operationele fout. Op basis van het resultaat kun je het proces afsluiten en opnieuw starten om onverwacht gedrag te voorkomen.

8. Catch alle onverwerkte promise rejections (Node.js)

Net zoals je nooit alle mogelijke uitzonderingen kunt dekken, is de kans groot dat je mogelijke promise rejections misloopt. In tegenstelling tot uitzonderingen, genereren promise rejections echter geen fouten.

Dus mocht een belangrijke promise een rejection krijgen, dan is de kans aanwezig dat je geen waarschuwing zal zien, waardoor de kans bestaat dat je later onverwacht gedrag tegenkomt. Daarom is het van cruciaal belang om een fallback mechanisme te implementeren voor het afhandelen van de promise rejection.

Zo kun je dat doen:

const promiseRejectionCallback = error => {
    console.log("PROMISE REJECTED: " + String(error))
}

process.on('unhandledRejection', callback)

Samenvatting

Net als elke andere programmeertaal komen fouten vrij vaak en natuurlijk voor in JavaScript. In sommige gevallen moet je zelfs opzettelijk fouten genereren om de juiste reactie aan je gebruikers aan te geven. Daarom is het van cruciaal belang om hun anatomie en soorten te begrijpen.

Bovendien moet je uitgerust zijn met de juiste tools en technieken om fouten te identificeren en te voorkomen dat je applicatie onbruikbaar wordt.

In de meeste gevallen is een solide strategie om fouten met zorgvuldige uitvoering af te handelen voldoende voor alle soorten JavaScript applicaties.

Zijn er nog andere JavaScript fouten die je nog steeds niet hebt kunnen oplossen? Zijn er technieken om constructief met JS fouten om te gaan? Laat het ons weten in de comments hieronder!

Kumar Harsh

Kumar is a software developer and a technical author based in India. He specializes in JavaScript and DevOps. You can learn more about his work on his website.