# Foutafhandeling in Apps Script [[TOC]] ## Basis try/catch/finally Apps Script draait op de V8-runtime en gebruikt de standaard JavaScript-foutafhandeling. Elke onafgevangen fout stopt de scriptuitvoering en verschijnt in het uitvoeringslogboek (Execution log) in de editor. ```javascript function basisFoutafhandeling() { try { const data = haalDataOpVanApi(); verwerkData(data); } catch(e) { Logger.log(`Fout: ${e.message}`); Logger.log(`Stack: ${e.stack}`); throw e; } finally { Logger.log('Altijd uitgevoerd, ook bij fout'); } } function haalDataOpVanApi() { const response = UrlFetchApp.fetch('https://api.voorbeeld.nl/data', { muteHttpExceptions: true, }); if (response.getResponseCode() !== 200) { throw new Error(`API-fout: HTTP ${response.getResponseCode()}`); } return JSON.parse(response.getContentText()); } ``` :::warn title="Zet muteHttpExceptions altijd aan" Zonder `muteHttpExceptions: true` gooit `UrlFetchApp.fetch()` zelf een fout bij een HTTP-status van 400 of hoger, voordat jij de statuscode kunt lezen. Met de optie aan krijg je het volledige response-object terug en bepaal je zelf hoe je per statuscode reageert. ::: ## Aangepaste fouttypen Door eigen fouttypen te definieren kun je in een catch-blok gericht reageren op het soort fout. Zet bijvoorbeeld een vlag `herstelbaar` op de fout, zodat een retry-laag later weet of opnieuw proberen zinvol is. ```javascript class ValidationFout extends Error { constructor(veld, bericht) { super(`Validatiefout in ${veld}: ${bericht}`); this.name = 'ValidationFout'; this.veld = veld; } } class ApiVerbindingsFout extends Error { constructor(statusCode, url) { super(`API fout ${statusCode} bij ${url}`); this.name = 'ApiVerbindingsFout'; this.statusCode = statusCode; this.url = url; this.herstelbaar = statusCode >= 500; } } function verwerkInputData(data) { if (!data.naam || data.naam.trim() === '') { throw new ValidationFout('naam', 'Naam is verplicht'); } if (!data.email || !data.email.includes('@')) { throw new ValidationFout('email', 'Ongeldig e-mailadres'); } } ``` :::info title="Controleer altijd e.name" De V8-runtime ondersteunt class-gebaseerde fouttypen volledig, inclusief overerving van `Error`. Controleer in een catch-blok `e.name` om het type fout te bepalen. Vermijd `instanceof` over scriptgrenzen heen, want bij bibliotheken kan dat onbetrouwbaar zijn; een vergelijking op `e.name` werkt overal. ::: ## Fouttype-specifieke afhandeling ```javascript function verwerkMetFoutafhandeling(invoer) { try { verwerkInputData(invoer); const resultaat = verwerkVoorApi(invoer); return {succes: true, data: resultaat}; } catch(e) { if (e.name === 'ValidationFout') { Logger.log(`Validatiefout, geen retry nodig: ${e.message}`); return {succes: false, fout: e.message, herstelbaar: false}; } if (e.name === 'ApiVerbindingsFout') { if (e.herstelbaar) { Logger.log('Serverfout, retry mogelijk'); return {succes: false, fout: e.message, herstelbaar: true}; } Logger.log('Clientfout, geen retry'); return {succes: false, fout: e.message, herstelbaar: false}; } Logger.log(`Onverwachte fout: ${e.message}`); throw e; } } ``` ## Retry-logica voor tijdelijke fouten Tijdelijke (transiente) fouten zoals een 500-status of een tijdelijke timeout lossen vaak vanzelf op. Een retry met oplopende wachttijd (exponentiele of lineaire backoff) maakt je script veerkrachtig zonder dat je een externe API overbelast. ```javascript function metRetry(fn, maxPogingen = 3, wachtMs = 1000) { let laatsteFout; for (let poging = 1; poging <= maxPogingen; poging++) { try { return fn(); } catch(e) { laatsteFout = e; const herstelbaar = e.name === 'ApiVerbindingsFout' && e.herstelbaar; if (!herstelbaar || poging === maxPogingen) { break; } Logger.log(`Poging ${poging} mislukt: ${e.message}. Wachten ${wachtMs * poging}ms`); Utilities.sleep(wachtMs * poging); } } throw laatsteFout; } function roepApiAanMetRetry() { return metRetry(() => { const response = UrlFetchApp.fetch('https://api.voorbeeld.nl/data', {muteHttpExceptions: true}); if (response.getResponseCode() >= 500) { throw new ApiVerbindingsFout(response.getResponseCode(), 'https://api.voorbeeld.nl/data'); } return JSON.parse(response.getContentText()); }); } ``` :::tip title="Let op de uitvoeringslimiet" Een Apps Script-uitvoering mag maximaal zes minuten draaien (dertig minuten voor sommige Workspace-abonnementen). Bouw je retry-laag zo dat de som van alle wachttijden ruim binnen die limiet blijft, anders breekt het script af met de melding "Exceeded maximum execution time" terwijl je nog aan het wachten bent. ::: ## Batch-verwerking met gedeeltelijke foutafhandeling Bij het verwerken van een lijst wil je vaak dat een fout in een enkel item de hele batch niet stopt. Vang per item op, verzamel de resultaten en rapporteer aan het eind wat geslaagd en wat mislukt is. ```javascript function verwerkBatchMetFouten(items) { const resultaten = { geslaagd: [], mislukt: [], totaal: items.length, }; items.forEach((item, idx) => { try { const resultaat = verwerkEnkelItem(item); resultaten.geslaagd.push({idx, item, resultaat}); } catch(e) { resultaten.mislukt.push({idx, item, fout: e.message}); Logger.log(`Item ${idx} mislukt: ${e.message}`); } }); Logger.log(`Geslaagd: ${resultaten.geslaagd.length}, Mislukt: ${resultaten.mislukt.length}`); return resultaten; } function verwerkEnkelItem(item) { if (!item.naam) throw new Error('Naam ontbreekt'); return {verwerkt: true, naam: item.naam}; } ``` ## Globale foutafhandelaar voor triggers Bij een tijdgestuurde trigger is er niemand die het foutscherm ziet. Wikkel de logica daarom in een wrapper die fouten opvangt, naar Cloud Logging schrijft met `console.error()` en de beheerder mailt. De wrapper gooit zelf geen fout, zodat de trigger-run netjes eindigt. ```javascript function triggerWrapper(fn, functieNaam) { const startTijd = Date.now(); try { fn(); Logger.log(`${functieNaam} succesvol in ${Date.now() - startTijd}ms`); } catch(e) { const melding = `Trigger fout in ${functieNaam}: ${e.message}`; console.error(melding); try { GmailApp.sendEmail( PropertiesService.getScriptProperties().getProperty('ADMIN_EMAIL') || 'admin@bedrijf.nl', `Script Fout: ${functieNaam}`, `${melding} Stack: ${e.stack}` ); } catch(mailFout) { console.error('Kon foutmelding niet verzenden: ' + mailFout.message); } } } function dagelijkseVerwerking() { triggerWrapper(() => { Logger.log('Dagelijkse verwerking gestart'); }, 'dagelijkseVerwerking'); } ``` Berichten die je met `console.error()` schrijft, verschijnen in Cloud Logging (voorheen Stackdriver) en in het Executions-paneel van de editor. Vink in de Google Cloud-instellingen van je project "Log uncaught exceptions" aan om onafgevangen fouten ook automatisch in Error Reporting te zien. :::howto title="Robuuste trigger met foutafhandeling" 1. Maak een `triggerWrapper(fn, naam)` die alle fouten opvangt. 2. Log fouten naar Cloud Logging met `console.error()`. 3. Stuur een e-mail naar de beheerder bij kritieke fouten. 4. Schrijf een audit-regel naar een log-sheet voor latere analyse. 5. Gooi de fout NIET opnieuw, zodat de trigger-run succesvol eindigt en niet herhaaldelijk opnieuw wordt geprobeerd. ::: :::faq ### Hoe stop ik een script netjes bij een kritieke fout? Gebruik `throw new Error('beschrijving')` om de uitvoering te stoppen en een foutmelding te loggen. Apps Script logt onafgevangen fouten automatisch in het uitvoeringslogboek. ### Wat is het verschil tussen throw en return bij fouten? `throw` stopt de uitvoering en geeft de fout door naar boven. `return` geeft een waarde terug, bijvoorbeeld `{succes: false}`. Gebruik `throw` voor onverwachte of kritieke fouten en `return` voor verwachte negatieve uitkomsten die de aanroeper rustig kan afhandelen. ### Hoe voorkom ik dat een fout in een trigger mijn script volledig stopt? Gebruik een `triggerWrapper()` die alle fouten opvangt, logt en notificeert maar zelf geen fout gooit. De trigger-run eindigt dan netjes en wordt niet steeds opnieuw uitgevoerd. ### Kan ik Error-subklassen gebruiken in Apps Script? Ja. De V8-runtime ondersteunt volledige ES6-class-hierarchieen, inclusief overerving van `Error`. Zet eigen velden op de subklasse, zoals een statuscode of een vlag `herstelbaar`. ### Waarom zie ik mijn fout niet in het logboek? Controleer of je niet per ongeluk de fout opvangt en negeert in een leeg catch-blok. Gebruik daarnaast `console.error()` in plaats van alleen `Logger.log()` voor fouten, want `console`-berichten blijven via Cloud Logging langer bewaard dan de korte logregels van `Logger`. ### Hoe handel ik een API-aanroep af die soms een 429 (te veel verzoeken) geeft? Behandel een 429 als tijdelijke fout en doe een retry met oplopende wachttijd. Respecteer waar mogelijk de `Retry-After`-header uit het response en blijf binnen de uitvoeringslimiet van het script. ::: Goede foutafhandeling is het verschil tussen een productie-script en een fragiel experiment. Investeer in consistente patronen voor alle scripts die automatisch worden uitgevoerd, dan weet je bij elke trigger-run precies wat er goed en wat er fout ging.