Feilhåndtering og feilkoder

Red: 13. juni 2026 | Pub: 15. mai 2026 | #node, #programmering

Med serverstatuskoder, «try/catch»-blokker og bedre fordeling av ansvar i koden, vil «to-do»-applikasjonen være bedre rustet til å håndtere feil både i selve programmet og i innkommende data fra klienten.

I forrige Node-innlegg opprettet jeg en server med Nodes egen HTTP-modul, som overvåket alle innkommende henvendelser og behandlet dem.

Men hva om noen ber om sletting av en oppgave som ikke finnes? Eller sender manglende og kanskje feil data? Det er applikasjonens oppgave å fange opp dette, og returnere en korrekt og tilfredsstillende beskjed til klienten, som kan forstå hva som har skjedd.

Serverkoder

Vi kan sende en konkret, manuelt forfattet beskjed, men vi bør også sende en standard relevant server-kode, som 200, 404 eller 503. Det finnes flere hundre koder for ulike tilstander , men i denne enkle applikasjonen trenger vi bare en håndfull, og kort fortalt er 2xx suksessmelding, 4xx klientfeil og 5xx serverfeil.

Koden min var derfor på dette stadiet problematisk, siden jeg hadde etablert en «header» som returnerte 200, uavhengig av typen forespørsel. Det er selvsagt ikke god praksis.

//...
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  if (req.method === "GET" && req.url === "/tasks") {
    const taskObjects = validateFile();
    res.end(JSON.stringify({
      data: taskObjects,
    }));
// ...

writeHead må defineres i de respektive endepunktene.

Jeg startet med slettingen av en oppgave. Den så slik ut:

else if (req.method === "DELETE" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
    const taskObjects = validateFile();
    deleteTask(Number(req.url.split("/")[2]), taskObjects);
    res.end(JSON.stringify({
        data: 'DELETING',
    }));
}

Jeg definerte først to variabler: en for responsmeldingen resObj, og en for statuskoden statuscode. Verdien bestemmes ut ifra responsen fra deleteTask-funksjonen:

else if (req.method === "DELETE" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
    const taskObjects = validateFile();
    const taskID = Number(req.url.split("/")[2]);
    let resObj = {};
    let statusCode;
    if (deleteTask(taskID, taskObjects)) {
        resObj.data = `Success. To-do with ID ${taskID} deleted`;
        statusCode = 200;
    } else {
        resObj.data = `Error. No to-do with ID ${taskID} found`;
        statusCode = 404;
    }
    res.writeHead(statusCode, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(resObj));
}

Ekstra feilhåndtering i funksjoner

Jeg måtte også oppdatere deleteTask-funksjonen til å returnere en booleansk verdi: true hvis ID-en eksisterte og oppgaven ble slettet, og false dersom ID-en er ukjent.

// delete a task
function deleteTask(id, taskObjects) {
  const checkForID = taskObjects.filter(obj => obj.id === id);
  if (checkForID.length) {
    taskObjects = taskObjects.filter((obj) => obj.id != id);
    writeFile(taskObjects);
    return true;
  }
  return false;
}

Jeg oppdaterte PUT og POST og henholdsvis checkTask- og taskObjectGenerator-funksjonene. Legg merke til at jeg i taskObjectGenerator sjekker om data eksisterer, og at den ikke er en tom streng.

I PUT og POST har jeg også lagt til en ekstra sjekk av typen data som kommer inn, for å sikre at det er henholdsvis boolsk verdi, og streng.

// ...

// check or uncheck task
function checkTask(id, status, taskObjects) {
  const checkForID = taskObjects.filter(obj => obj.id == id);
  if (checkForID.length) {
	taskObjects = taskObjects.map(task => {
        if(task.id == id) {
            return {...task, "done": status};
        } else {
	        return task;    
        }
    });
    writeFile(taskObjects);
    return true;
  };
  return false;
}

// Create todo-object
function taskObjectGenerator(data, taskObjects) {
  if (data && data.trim()) {
    const object = {
      "createdDate": new Date().toISOString().split("T")[0],
      "done": false,
      "id": Date.now(),
      "task": data,
    }
    taskObjects.push(object);
    writeFile(taskObjects);
    return true;
  }
  return false;
};

// ...

else if (req.method === "POST" && req.url === "/tasks") {
    let body = "";
    req.on("data", (chunk) => {
        body += chunk.toString();
    });
    req.on("end", () => {
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        let resObj = {};
        let statusCode;
        if (typeof data.task === "string" && taskObjectGenerator(data.task, taskObjects)) {
          resObj.data = `Success. To-do ${data.task} created`;
          statusCode = 200;
        } else {
          resObj.data = `Error. Creation of to-do ${data.task} failed. Name, date or ID is missing.`
          statusCode = 400;
        }
        res.writeHead(statusCode, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(resObj));
      });
  } else if (req.method === "PUT" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
      let body = "";
      req.on("data", (chunk) => {
          body += chunk.toString();
      });
      req.on("end", () => {
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        const taskID = Number(req.url.split("/")[2]);
        let resObj = {};
        let statusCode;
        if (typeof data.status === "boolean" && checkTask(taskID, data.status , taskObjects)) {
          resObj.data = `Success. To-do with ID ${taskID} changed to ${data.status}`;
          statusCode = 200;
        } else {
          resObj.data = `Error. No to-do with ID ${taskID} found.`;
          statusCode = 404;
        }
        res.writeHead(statusCode, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(resObj));
    });
} 
// ...

Til slutt, i if-else-blokken, la jeg inn en 404:

else {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      data: 'Error. Page not found'
    }));
}

Og øverst i GET en 200. Denne kan sløyfes, siden status 200 er standard hvis ikke annet er angitt, men det er alltid bedre å være eksplisitt:

 if (req.method === "GET" && req.url === "/tasks") {
    const taskObjects = validateFile();
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      data: taskObjects,
    }));
  }

try/catch

Nå har jeg en sjekk for dataene som kommer fra klienten, at den ikke er feil eller mangler. Men hva om noe skjer med serveren eller i applikasjonen? Hva skal da returneres?

Da benyttes «try/catch». Hvis noe i try-blokken feiler, og gir en «exception», altså en uventet feil som avbryter programmet, så sendes den til «catch»-blokken som tar over, og gir oss en mulighet til å gi beskjed til klienten, med mindre det er en fatal error selvsagt, som krasjer hele serveren og/eller applikasjonen.

Jeg startet med POST, og flyttet alt i req.on("end", () => inn i en «try/catch»:

else if (req.method === "POST" && req.url === "/tasks") {
      let body = "";
      req.on("data", (chunk) => {
          body += chunk.toString();
      });
      req.on("end", () => {
        try {
          let resObj = {};
          let statusCode;
          const data = JSON.parse(body);
          const taskObjects = validateFile();
          if (typeof data.task === "string" && taskObjectGenerator(data.task, taskObjects)) {
            resObj.data = `Success. To-do ${data.task} created`;
            statusCode = 200;
          } else {
            resObj.data = `Error. Creation of to-do ${data.task} failed. Name, date or ID is missing.`
            statusCode = 400;
          }
          res.writeHead(statusCode, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify(resObj));
        } catch (error) {
          res.writeHead(error.name === "SyntaxError" ? 400 : 500, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({
            "error": `Something went wrong`
          }));
        }
    });
}

Merk at jeg ikke sender error-meldingen fra catch til klienten. Den kan inneholde sensitiv informasjon om serveren, og er heller ment til feillogg for systemadministrator/applikasjonsutvikler, noe jeg antagelig vil utforske i neste steg av utviklingen.

Jeg sender heller en manuell melding og statuskode. Først satte jeg koden til 500, men det ble ikke helt presist. 500 indikerer jo en serverfeil, og det stemmer ikke overens dersom det er klientens JSON som er ugyldig. Det kan vi løse med å sjekke navnet på error-meldingen:

res.writeHead(error.name === "SyntaxError" ? 400 : 500, { 'Content-Type': 'application/json' });

Jeg gjorde det samme med PUT, og oppdaterte til slutt DELETE:

else if (req.method === "PUT" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
      let body = "";
      req.on("data", (chunk) => {
          body += chunk.toString();
      });
      req.on("end", () => {
        try {
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        const taskID = Number(req.url.split("/")[2]);
        let resObj = {};
        let statusCode;
        if (typeof data.status === "boolean" && checkTask(taskID, data.status , taskObjects)) {
          resObj.data = `Success. To-do with ID ${taskID} changed to ${data.status}`;
          statusCode = 200;
        } else {
          resObj.data = `Error. No to-do with ID ${taskID} found.`;
          statusCode = 404;
        }
        res.writeHead(statusCode, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(resObj));
      } catch (error) {
          res.writeHead(error.name === "SyntaxError" ? 400 : 500, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({
            "error": `Something went wrong.`
          }));
        }
      });
   } else if (req.method === "DELETE" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
    try {
      const taskObjects = validateFile();
      const taskID = Number(req.url.split("/")[2]);
      let resObj = {};
      let statusCode;
      if (deleteTask(taskID, taskObjects)) {
        resObj.data = `Success. To-do with ID ${taskID} deleted`;
        statusCode = 200;
      } else {
        resObj.data = `Error. No to-do with ID ${taskID} found`;
        statusCode = 404;
      }
      res.writeHead(statusCode, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(resObj));
  } catch (error) {
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({
        "error": "Something went wrong."
      }));
    }
}

Alle varianter ble testet med Curl, og til min store fryd så det ut til å fungere.

Konvertering til Express

I og med at «try/catch» returnerer feilmeldinger som er ment for utviklere og serveradministratorer, tenker jeg at en loggføring av applikasjonen er et naturlig neste steg. Det finnes tredjeparts-moduler for dette som Winston, men den er laget spesielt for rammeverket Express, en annen tredjepartsmodul som erstatter HTTP-modulen. Derfor bør jeg først konvertere nåværende kode, før jeg starter på loggingen.

Og som alltid, her er den fullstendige så langt:

"use strict"
const http = require('node:http');
const fs = require("fs");
const tasksFileName = "tasks.json";

// Validate file
function validateFile() {
  // create file if it doesnt exist
  if (!fs.existsSync(`./${tasksFileName}`)) {
    fs.writeFileSync(tasksFileName, "[]")
  };
  return JSON.parse(fs.readFileSync(tasksFileName, "utf8"));
}

// create a backup
function backup() {
  fs.copyFileSync(tasksFileName, `${tasksFileName}.bak`);
}

// write files
function writeFile(taskObjects) {
  backup();
  fs.writeFileSync(tasksFileName, JSON.stringify(taskObjects, null, 2));
}

// check or uncheck task
function checkTask(id, status, taskObjects) {
  const checkForID = taskObjects.filter(obj => obj.id == id);
  if (checkForID.length) {
    taskObjects = taskObjects.map(task => {
      if(task.id == id) {
        return {...task, "done": status};
      } else {
        return task;    
      }
    });
    writeFile(taskObjects);
    return true;
  };
  return false;
}

// Create todo-object
function taskObjectGenerator(data, taskObjects) {
  if (data && data.trim()) {
    const object = {
      "createdDate": new Date().toISOString().split("T")[0],
      "done": false,
      "id": Date.now(),
      "task": data,
    }
    taskObjects.push(object);
    writeFile(taskObjects);
    return true;
  }
  return false;
};

// delete a task
function deleteTask(id, taskObjects) {
  const checkForID = taskObjects.filter(obj => obj.id === id);
  if (checkForID.length) {
    taskObjects = taskObjects.filter((obj) => obj.id != id);
    writeFile(taskObjects);
    return true;
  }
  return false;
}

// Opprett en lokal server som skal motta data
const server = http.createServer((req, res) => {
  if (req.method === "GET" && req.url === "/tasks") {
    const taskObjects = validateFile();
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      data: taskObjects,
    }));
  } else if (req.method === "POST" && req.url === "/tasks") {
    let body = "";
    req.on("data", (chunk) => {
      body += chunk.toString();
    });
    req.on("end", () => {
      try {
        let resObj = {};
        let statusCode;
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        if (typeof data.task === "string" && taskObjectGenerator(data.task, taskObjects)) {
          resObj.data = `Success. To-do ${data.task} created`;
          statusCode = 200;
        } else {
          resObj.data = `Error. Creation of to-do ${data.task} failed. Name, date or ID is missing.`
          statusCode = 400;
        }
        res.writeHead(statusCode, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(resObj));
      } catch (error) {
        res.writeHead(error.name === "SyntaxError" ? 400 : 500, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          "error": "Somethin went wrong."
        }));
      }
    });
  } else if (req.method === "PUT" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
    let body = "";
    req.on("data", (chunk) => {
      body += chunk.toString();
    });
    req.on("end", () => {
      try {
        const data = JSON.parse(body);
        const taskObjects = validateFile();
        const taskID = Number(req.url.split("/")[2]);
        let resObj = {};
        let statusCode;
        if (typeof data.status === "boolean" && checkTask(Number(taskID), data.status , taskObjects)) {
          resObj.data = `Success. To-do with ID ${taskID} changed to ${data.status}`;
          statusCode = 200;
        } else {
          resObj.data = `Error. No to-do with ID ${taskID} found.`;
          statusCode = 404;
        }
        res.writeHead(statusCode, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(resObj))
      } catch (error) {
        res.writeHead(error.name === "SyntaxError" ? 400 : 500, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          "error": `Something went wrong.`
        }));
      }
    });
  } else if (req.method === "DELETE" && req.url.startsWith("/tasks/") && req.url.split("/").length === 3) {
    try {
      const taskObjects = validateFile();
      const taskID = Number(req.url.split("/")[2]);
      let resObj = {};
      let statusCode;
      if (deleteTask(Number(taskID), taskObjects)) {
        resObj.data = `Success. To-do with ID ${taskID} deleted`;
        statusCode = 200;
      } else {
        resObj.data = `Error. No to-do with ID ${taskID} found`;
        statusCode = 404;
      }
      res.writeHead(statusCode, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(resObj))
    } catch (error) {
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({
        "error": "Something went wrong."
      }));
    }
  } else {
    res.writeHead(404, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      data: 'Error. Page not found'
    }));
  }
});


server.listen(8000);

Ris, ros eller respons?

Send meg gjerne om du har en kommentar, korrektur eller konstruktiv kritikk til denne saken.