Express alapok · 8. tananyag
Eddig minden végpontban külön írtuk le, mi történjen hiba esetén. Ez a fejezet egyetlen helyre vonja össze — és tisztázza azt a kérdést is, amit a 3. fejezetben félbehagytunk: az Express 5 tényleg elkapja az aszinkron hibákat, de pontosan mit, és mit nem.
unhandledRejection, uncaughtException);Nem minden hiba egyforma. A különbségtétel dönti el, mit csinálunk vele:
| Működési hiba | Programozói hiba | |
|---|---|---|
| Mi ez? | Számítottunk rá, csak nem történt meg | A kódunk hibás |
| Példa | Nincs ilyen termék · hibás JSON · nincs jogosultság | undefined mező olvasása · elgépelt függvénynév |
| Kinek szól? | A kliensnek — meg tudja oldani | A fejlesztőnek — javítani kell |
| Státuszkód | 4xx | 5xx |
| Mit mutatunk? | Pontos, hasznos üzenetet | Semmit — csak annyit, hogy hiba történt |
| Naplózás | Elég röviden | Teljes veremkiírással |
A 4xx azt jelenti: „te hibáztál, és meg tudod javítani”. Az
5xx azt: „mi hibáztunk, te nem tehetsz semmit”. Ha a kliens a
hibaüzenetből nem tud továbblépni, akkor rossz kódot adtál neki.
Hat helyzet a webshop API-ból. Válaszd ki a megfelelő státuszkódot — a 4xx/5xx döntés a lényeg.
Ha egyetlen útvonal sem illeszkedik, a lánc végére ér a kérés. Ott várja a 404-kezelő:
// src/middlewares/nem-talalhato.js export function nemTalalhato(req, res) { res.status(404).json({ error: { message: `Nincs ilyen végpont: ${req.method} ${req.originalUrl}`, code: "NINCS_VEGPONT" } }); }
Az eredmény:
GET /nincs → 404 {"error":{"message":"Nincs ilyen végpont: GET /nincs","code":"NINCS_VEGPONT"}}
Mert az app.get("*") Express 5-ben hibát dob (3. fejezet).
A minta nélküli app.use(fv) minden kérésre illeszkedik — pontosan ezt akarjuk
a lánc végén.
Mert a „nincs ilyen végpont” működési hiba: a kliens elgépelte az URL-t. Nem kell hozzá
a hibakezelő gépezete — elég egy egyszerű válasz. (Van, aki
next(new HttpHiba(404, …)) alakban a hibakezelőnek adja át; mindkét megoldás
elfogadott, mi az egyszerűbbet választjuk.)
Az Express beépítettje válaszol — HTML-lel:
404 text/html <!DOCTYPE html> … <pre>Cannot GET /nincs</pre>
Egy JSON API-nál ez rossz: a kliens JSON.parse hibára fut, és nem tudja meg,
mi történt.
Négy paraméter, a lánc legvégén — a 6. fejezetből már ismerős:
// src/middlewares/hibakezelo.js export function hibakezelo(err, req, res, next) { // 1) naplózás — mindig, mielőtt bármi mást tennénk console.error(`[HIBA] ${req.method} ${req.originalUrl}`, err); // 2) ha a válasz már elment, nincs mit tenni if (res.headersSent) { return next(err); } // 3) válasz a kliensnek const statusz = err.status ?? 500; res.status(statusz).json({ error: { message: statusz < 500 ? err.message : "Váratlan szerverhiba történt.", code: statusz < 500 ? (err.code ?? "SZERVERHIBA") : "SZERVERHIBA" } }); }
| Lépés | Miért? |
|---|---|
| 1. Naplózás | Ez az egyetlen hely, ahol minden hiba átmegy — itt éri meg naplózni |
2. res.headersSent | Ha a válasz már elment, a res.status() hívás újabb hibát okozna. Ilyenkor az Express beépített kezelőjének adjuk át, ami lezárja a kapcsolatot |
3. err.status ?? 500 | Ha a hiba tudja, milyen kódot érdemel, azt használjuk; ha nem, 500 |
A < 500 feltétel | 4xx-nél a hiba saját üzenete és kódja megy ki, 5xx-nél általános szöveg és SZERVERHIBA — a részletek csak a naplóba kerülnek. A kódot is le kell cserélni: a Node saját hibáinak is van kódja (kipróbálva: hiányzó fájlnál ENOENT), és az a belső működésről árulkodik |
?? ÉS NEM ||?
Mert az err.status || 500 a 0 értéket is
lecserélné — igaz, hogy 0-s státuszkód nincs, de a
?? fejezi ki pontosan a szándékot: „csak akkor, ha nincs megadva”.
Ugyanaz a különbség, mint a 4. fejezetben a query paramétereknél.
Mind a négy módon érkező hiba ideér — az eredmények:
| Ahogy a hiba keletkezik | Példa | Eredmény |
|---|---|---|
throw szinkron kezelőben | app.get("/x", () => { throw new Error(…) }) | 500 |
throw async kezelőben | app.get("/x", async () => { throw new Error(…) }) | 500 |
throw async middleware-ben | app.use(async (q, s, n) => { throw new Error(…) }) | 500 |
next(hiba) | app.get("/x", (q, s, n) => n(new Error(…))) | 500 |
A harmadik sor az Express 5 újdonsága — a 4-esben ez a hiba elveszett volna.
Ahhoz, hogy a hibakezelő tudja, milyen státuszkód és hibakód jár, a hibának magával kell hoznia ezt az információt:
// src/utils/HttpHiba.js export class HttpHiba extends Error { constructor(status, message, code) { super(message); this.name = "HttpHiba"; this.status = status; this.code = code; } }
| Elem | Mit csinál? |
|---|---|
extends Error | Örökli a message mezőt és a veremkiírást (stack) |
super(message) | Átadja az üzenetet az Error konstruktorának — enélkül üres lenne |
this.name | Ez jelenik meg a naplóban a hiba neveként |
this.status | A státuszkód, amit a hibakezelő kiolvas |
this.code | Az állandó, gépnek szóló kód (5. fejezet) |
throw előnye a return res.…-szel szembenreturn res.status(…)A RETURN FELEJTHETŐapp.get("/api/products/:id", (req, res) => { const id = Number(req.params.id); if (Number.isNaN(id)) { return res.status(400).json({ error: { message: "…", code: "…" } }); } const termek = keres(id); if (!termek) { return res.status(404).json({ error: { message: "…", code: "…" } }); } res.json(termek); });
throw new HttpHiba(…)RÖVIDEBBapp.get("/api/products/:id", (req, res) => { const id = Number(req.params.id); if (Number.isNaN(id)) { throw new HttpHiba(400, "Az azonosító csak szám lehet.", "ERVENYTELEN_ID"); } const termek = keres(id); if (!termek) { throw new HttpHiba(404, "Nincs ilyen termék.", "TERMEK_NEM_TALALHATO"); } res.json(termek); });
Az eredmény:
GET /sajat → 404 {"nev":"HttpHiba","uzenet":"Nincs ilyen termék.","kod":"TERMEK_NEM_TALALHATO"}
throw AZONNAL MEGSZAKÍTJA A FÜGGVÉNYT
A return felejthető (5. fejezet:
ERR_HTTP_), a throw viszont
nem hagy rá lehetőséget. Ez a fejezet egyik legfontosabb gyakorlati haszna.
Rögzítsük őket egy helyen, hogy a kliens is dokumentálva kapja:
| Kód | Státusz | Mikor? |
|---|---|---|
ERVENYTELEN_ID | 400 | Az azonosító nem szám vagy nem pozitív |
HIANYZO_MEZO | 400 | Kötelező mező hiányzik a törzsből |
ERVENYTELEN_JSON | 400 | A törzs nem érvényes JSON |
TUL_NAGY_TORZS | 413 | A törzs nagyobb a megengedettnél |
TERMEK_NEM_TALALHATO | 404 | Nincs ilyen azonosítójú termék |
NINCS_VEGPONT | 404 | Nincs ilyen útvonal |
SZERVERHIBA | 500 | Minden más |
Ez a fejezet legfontosabb — és legtöbb meglepetést okozó — szakasza.
app.get("/api/products", async (req, res) => { const termekek = await adatbazisbolLekerdez(); // ha ez elhasal… res.json(termekek); // …ide nem jut el });
Az Express 5 az async függvény által visszaadott Promise elutasítását
magától a hibakezelőhöz továbbítja. Kipróbálva: 500-as válasz
születik a saját hibakezelőnkből.
Két eset, és mindkettő csendben okoz bajt. Az eredmények:
app.get("/promise", (req, res) => { Promise.reject(new Error("elkapatlan promise")); // ❌ nincs await, nincs return });
!! unhandledRejection: elkapatlan promise GET /promise → IDŐTÚLLÉPÉS — a kérés lóg
A kérés soha nem kap választ, mert a kezelőfüggvény sem válaszolt, sem
next()-et nem hívott. Az Express nem lát semmit: a Promise nem az ő kezében
van.
app.get("/timeout", (req, res) => { setTimeout(() => { throw new Error("callbackben dobott hiba"); // ❌ senki nem kapja el }, 10); res.json({ elment: true }); });
GET /timeout → 200 {"elment":true} !! uncaughtException: callbackben dobott hiba
A válasz elment, a hiba viszont kiszökött a folyamatba. Kezeletlen
uncaughtException esetén a Node alapból leállítja a
programot.
Az Express csak azt látja, amit a kezelőfüggvény visszaad. Ha egy aszinkron művelet elé
nem írsz await-et (vagy nem adod vissza return-nel),
az Express számára nem is létezik.
Nyolc kódrészlet. Döntsd el mindegyikről, hogy a hiba eljut-e a központi hibakezelőhöz — a visszajelzés megadja a következményt és a javítást is.
try/catch?Nem mindenhova. Két esetben igen:
// 1) Ha SAJÁT üzenetet vagy más státuszkódot akarsz app.get("/api/products/:id", async (req, res) => { try { const termek = await szolgaltatas.termekAzonositoval(id); res.json(termek); } catch (hiba) { throw new HttpHiba(503, "Az adatbázis nem érhető el.", "ADATBAZIS_HIBA"); } }); // 2) Ha takarítani kell (fájl bezárása, tranzakció visszagörgetése)
Ha viszont csak annyit akarsz, hogy a hibából 500 legyen,
hagyd el — az Express elintézi.
A két „nem elkapott” esetre a Node ad utolsó védővonalat. Ez az index.js-be
kerül:
// index.js import app from "./src/app.js"; import { config } from "./src/config.js"; const szerver = app.listen(config.port, () => { console.log(`A szerver fut (${config.kornyezet}): http://localhost:${config.port}`); }); process.on("unhandledRejection", (hiba) => { console.error("[VÉGZETES] Kezeletlen Promise-elutasítás:", hiba); szerver.close(() => process.exit(1)); }); process.on("uncaughtException", (hiba) => { console.error("[VÉGZETES] Kezeletlen kivétel:", hiba); szerver.close(() => process.exit(1)); });
| Esemény | Mikor lép működésbe? |
|---|---|
unhandledRejection | Egy Promise elutasításra került, és senki nem kapta el |
uncaughtException | Egy throw kiszökött minden try/catch-ből |
Mert ilyenkor a program ismeretlen állapotba került: nem tudjuk, mi futott le és mi nem.
Tovább futtatni kockázatosabb, mint újraindítani. A szerver.close() előbb
kiszolgálja a már futó kéréseket, csak utána lép ki — ezt hívják kecses leállásnak
(graceful shutdown).
Éles környezetben egy felügyelő program (a szolgáltató vagy egy folyamatkezelő) automatikusan
újraindítja a szervert. Iskolai gépen a node --watch ugyanezt teszi.
Az Express beépített hibakezelője már magától így viselkedik. Az eredmények ugyanarra a hibára:
A saját hibakezelőnknek is így kell tennie. A 7. fejezet config.js-e alapján:
import { config } from "../config.js"; export function hibakezelo(err, req, res, next) { console.error(`[HIBA] ${req.method} ${req.originalUrl}`, err); if (res.headersSent) return next(err); const statusz = err.status ?? 500; const fejlesztes = config.kornyezet !== "production"; res.status(statusz).json({ error: { message: statusz < 500 ? err.message : "Váratlan szerverhiba történt.", code: statusz < 500 ? (err.code ?? "SZERVERHIBA") : "SZERVERHIBA", ...(fejlesztes && statusz >= 500 ? { reszletek: err.message, stack: err.stack } : {}) } }); }
| Rész | Mit csinál? |
|---|---|
statusz < 500 ? … : … | 4xx-nél a pontos üzenet és kód megy ki, 5xx-nél általános szöveg és SZERVERHIBA (3. szakasz) |
...(feltétel ? { … } : {}) | Feltételes mezők: csak akkor kerülnek bele az objektumba, ha a feltétel igaz |
reszletek és stack | Csak fejlesztéskor, és csak 5xx-nél |
Mert a hibaüzenet elárulhatja a belső szerkezetet: fájlneveket, mappaneveket, adatbázis-mezőket, néha még a lekérdezést is. Ez támadási felület. A fejlesztő a naplóból úgyis megkapja a teljes képet.
webshop-api/ index.js ← app.listen + a process-szintű hálók src/ app.js ← a lánc összeállítása config.js middlewares/ nem-talalhato.js ← 404 hibakezelo.js ← a központi hibakezelő utils/ HttpHiba.js ← a saját hibaosztály routes/ controllers/ services/
// src/app.js — a teljes lánc import express from "express"; import path from "node:path"; import termekRouter from "./routes/termek.routes.js"; import { nemTalalhato } from "./middlewares/nem-talalhato.js"; import { hibakezelo } from "./middlewares/hibakezelo.js"; const app = express(); app.use(express.json()); // 1) törzs app.use(express.static(path.join(import.meta.dirname, "..", "public"))); // 2) statikus app.use("/api/products", termekRouter); // 3) útvonalak app.use(nemTalalhato); // 4) 404 app.use(hibakezelo); // 5) hibakezelő export default app;
| Kérés | Státusz | Válasz |
|---|---|---|
GET /api/products/42 | 200 | a termék |
GET /api/products/alma | 400 | {"error":{"message":"Az azonosító csak szám lehet.","code":"ERVENYTELEN_ID"}} |
GET /api/products/999 | 404 | {"error":{"message":"Nincs ilyen termék.","code":"TERMEK_NEM_TALALHATO"}} |
GET /nincs-ilyen | 404 | {"error":{"message":"Nincs ilyen végpont: GET /nincs-ilyen","code":"NINCS_VEGPONT"}} |
POST hibás JSON-nal | 400 | a törzs-feldolgozó hibája, ugyanabban a formátumban |
| bármi váratlan | 500 | {"error":{"message":"Váratlan szerverhiba történt.","code":"SZERVERHIBA"}} |
Minden válasz ugyanabban a formátumban — ez az egész fejezet célja.
app.use(hibakezelo); // ❌ az útvonalak ELŐTT app.use("/api/products", termekRouter);
Tünet: a hibaválasz HTML, a saját JSON-formátumod helyett.
Megoldás: a hibakezelő a legvégére, a 404-kezelő után.
app.use((err, req, res) => { … }); // ❌ hiányzik a next
Tünet: ugyanaz — az Express közönséges middleware-nek nézi, és soha nem ad neki hibát.
Megoldás: mind a négy paraméter kiírása, akkor is, ha a next-et
nem használod.
app.use(hibakezelo); app.use(nemTalalhato); // ❌
Tünet: a nem létező végpontokra a 404 megy ugyan, de a hibák is oda futnak
be, mert a hibakezelő már lefutott előtte.
Megoldás: először nemTalalhato, aztán
hibakezelo.
unhandledRejection a naplóbanapp.get("/x", (req, res) => { szolgaltatas.valamiAszinkron(); // ❌ nincs await, nincs return });
Megoldás: async kezelőfüggvény +
await, vagy return a Promise elé.
ERR_HTTP_HEADERS_SENT a hibakezelőbenHa a válasz már elment, mire a hiba keletkezett, a hibakezelő újabb hibát okozna.
Megoldás: a res.headersSent vizsgálata a kezelő elején.
Tünet: a 500-as válaszban ott a fájlnév és a sor száma.
Megoldás: a NODE_ENV=production beállítása és a részletek
elrejtése a hibakezelőben.
Elméleti
Sorold be mindegyiket, és indokold:
nev mezőundefined mezőre hivatkoztálRakd helyes sorrendbe: hibakezelo,
express.json(),
app.use("/api/products", router),
nemTalalhato, helmet(). Mindegyikhez írj egy
mondatot, miért ott a helye!
Döntsd el mindegyikről, hogy az Express 5 a hibakezelőhöz továbbítja-e, és indokold:
a) app.get("/a", () => { throw new Error("x"); }); b) app.get("/b", async () => { throw new Error("x"); }); c) app.get("/c", (q, s) => { Promise.reject(new Error("x")); }); d) app.get("/d", (q, s) => { setTimeout(() => { throw new Error("x"); }, 10); }); e) app.get("/e", (q, s, n) => n(new Error("x")));
Mi a baj, és mi a tünete? (Két hibát keress: egy szerkezetit és egy biztonságit.)
export function hibakezelo(err, req, res) { res.status(err.status || 500).json({ error: { message: err.message } }); }
Egészítsd ki a 4. szakasz kódtáblázatát a rendelésekhez tartozó hibákkal (üres kosár, nem elérhető készlet, már kifizetett rendelés módosítása, más felhasználó rendelése). Mindegyikhez add meg a státuszkódot, a kódot és az üzenetet, és indokold a státuszkód-választást!
Gyakorlati
Készítsd el a nemTalalhato middleware-t a saját formátumodban, és
bizonyítsd be REST Clienttel, hogy egy nem létező végpont JSON-t ad vissza, nem HTML-t.
Írd meg a hibakezelo middleware-t a 3. szakasz szerint (naplózás,
headersSent-vizsgálat, err.status). Készíts egy
szándékosan hibás végpontot (GET /api/bumm), és nézd meg, mit kap a kliens
és mit ír a terminál!
Készítsd el a HttpHiba osztályt, és írd át vele a
GET /api/products/:id végpontot úgy, hogy
return res.status(…) helyett throw-t használjon.
Ellenőrizd, hogy mindhárom eset (200, 400,
404) továbbra is helyesen működik!
Készíts egy async végpontot, amely egy szándékosan elhasaló műveletet hív
(await Promise.reject(new Error("teszt"))). Bizonyítsd be, hogy
try/catch nélkül is
500-as választ kapsz a saját hibakezelődből!
Egészítsd ki a hibakezelőt úgy, hogy 5xx esetén csak fejlesztéskor küldje el
a reszletek és stack mezőket. Indítsd el a
szervert egyszer NODE_ENV=development, egyszer
NODE_ENV=production beállítással, és hasonlítsd össze a két választ!
Állítsd be a process-szintű hálókat is.
4xx, a kliens megoldhatja)
és a programozói (5xx, a fejlesztőnek szól). A
státuszkód-választás ebből következik.res.headersSent értékét, majd egységes
JSON-választ küld.HttpHiba osztály
status és code mezőt hoz magával — így a
throw new HttpHiba(404, …) írásmód elég, és nem felejthető el a
return.async kezelőkben,
valamint az async middleware-ekben dobott hibákat, és a
next(hiba) hívást.setTimeout-visszahívásban dobott hibát (a folyamat
leállhat).unhandledRejection, uncaughtException),
amelyek naplóznak, majd kecsesen leállítják a szervert.5xx-es válasz nem tartalmazhat
üzenetet és veremkiírást — azok csak a naplóba mennek.| Fogalom | Jelentés |
|---|---|
| Működési hiba | Számítottunk rá; a kliens meg tudja oldani. 4xx |
| Programozói hiba | A kódunk hibás; a kliens nem tehet semmit. 5xx |
| 404-kezelő | Minta nélküli middleware a lánc végén, az útvonalak után |
| Központi hibakezelő | Négyparaméteres middleware a lánc legvégén |
err.status | A hibához tartozó státuszkód — a beépített kezelő is ezt használja |
err.code | Az állandó, gépnek szóló hibakód |
res.headersSent | Igaz, ha a válasz már elment |
unhandledRejection | Node-esemény: elutasított Promise, amit senki nem kapott el |
uncaughtException | Node-esemény: minden try/catch-ből kiszökött hiba |
| Kecses leállás | A futó kérések kiszolgálása, majd kilépés (szerver.close()) |
NODE_ENV | Környezeti változó; a production érték elrejti a hibarészleteket |