8.3 Hibakezelés és hibaüzenetek
Áttekintő
Minden szoftver hibázik. A kérdés nem az, hogy lesz-e hiba, hanem hogy hogyan kezeled, amikor bekövetkezik.
Az egyik leggyakoribb junior hiba: a program elszáll, a stack trace egyből a felhasználó arcába robban, és ott áll egy belső szerver hiba leírásával, fájlnevekkel, kódsorokkal – amit el se kellene látnia. Közben a fejlesztő sem tudja, mi történt, mert nem naplóztuk megfelelően.
A production-ready kód két feladatot old meg egyszerre:
- A fejlesztőt informálja, hogy megtalálja és javítsa a hibát
- A felhasználót védi, hogy ne lásson belső rendszerinformációt
Ez a kettő egyszerre teljesíthető – és ebben a fejezetben megmutatom hogyan.
Miért számít ez az állásinterjún?
A senior fejlesztők pontosan azt nézik a kódodban, hogy kezeled-e a hibákat – nem csak azt, hogy működik-e a happy path. Egy PR review-ban az első kérdés sokszor: „Mi történik, ha ez az API hívás meghiúsul?"
Részletes leírás
1. Miért veszélyes a nyers stack trace a felhasználónak?
Nézzük meg, mi szerepelhet egy tipikus stack trace-ben:
Internal Server Error
Traceback (most recent call last):
File "/app/services/payment.py", line 47, in process_payment
result = db.query("SELECT * FROM payments WHERE user_id=" + user_id)
sqlalchemy.exc.OperationalError: (psycopg2.OperationalError)
connection to server at "db-prod-eu-west-1.internal" (10.0.1.45)
port 5432 failed: Connection refused
Ez az egyetlen stack trace egyszerre elárulja:
- Hogy Python-t és SQLAlchemy-t használtok
- A belső IP-cím és port, ahol az adatbázis fut
- A production szerver neve és régió utalása
- Hogy a
paymentstábla létezik - Potenciálisan SQL injection sérülékenységet (string concatenation!)
Egy támadónak ez arany. A felhasználónak értéktelen.
A szabály: Amit a felhasználó lát, az soha nem tartalmazhat belső rendszerinformációt.
2. Try-catch elhelyezése: mit fogj el és mit ne?
A leggyakoribb junior hiba: mindent try-catch-be csomagol, vagy sehova nem tesz hibakezelést.
Az "anti-pattern": mindent elnyel
# ROSSZ - elnyelő catch
try:
result = process_payment(user_id, amount)
send_email(user, "Sikeres fizetés")
update_inventory(items)
except Exception:
pass # "nem fog hibázni"
Ez a kód csendben elnyeli az összes hibát. Ha a fizetés sikertelen, a felhasználó mégsem kap emailt – de nem tudod miért, mert nincs log, nincs hibaüzenet, semmi.
Az "anti-pattern": sehol nincs kezelés
// ROSSZ - nincs hibakezelés
async function getUserData(userId) {
const user = await db.findById(userId);
const orders = await fetchOrdersFromAPI(user.email);
return { user, orders };
}
Ha az API hívás meghiúsul (pl. timeout), az exception buborékol felfelé, végül a felhasználó egy értelmetlen hibaoldalt lát.
A helyes megközelítés: rétegelt hibakezelés
A hibakezelés nem egy helyen, hanem rétegekben működik:
1. réteg – Üzleti logika szintje: specifikus hibákat fogj el, amelyekre tudsz reagálni
def process_payment(user_id: int, amount: float):
try:
result = payment_gateway.charge(user_id, amount)
return result
except PaymentDeclinedException as e:
# Ezt a hibát tudjuk kezelni: informáljuk a felhasználót
raise UserFacingError("A fizetés elutasítva. Ellenőrizd a kártyaadataid.")
except PaymentGatewayTimeoutException as e:
# Infrastruktúra hiba: logolj és dobáld tovább
logger.error(f"Payment gateway timeout for user {user_id}: {e}")
raise ServiceUnavailableError("A fizetési rendszer átmenetileg nem elérhető.")
2. réteg – Alkalmazás szintje (global error handler): mindent, ami nem lett kezelve, itt kapod el
# Flask példa - globális hibakezelő
@app.errorhandler(Exception)
def handle_unexpected_error(error):
logger.exception(f"Unexpected error: {error}") # Teljes stack trace a logba
return jsonify({
"error": "Váratlan hiba történt. Kérjük, próbáld újra később."
}), 500
Kulcsgondolat: Specifikus hibákat kezeld ott, ahol tudod mit jelent. Az ismeretlen hibákat engedd felfelé, ahol egy globális handler elkapja és loggolja.
3. Felhasználói vs. fejlesztői hibaüzenet
Ez a két üzenet soha nem lehet ugyanaz.
| Felhasználói üzenet | Fejlesztői üzenet (log) | |
|---|---|---|
| Tartalom | Emberi, érthető | Technikai, részletes |
| Cél | Tájékoztatás, következő lépés | Diagnosztika, nyomozás |
| Stack trace | SOHA | Mindig |
| Belső adat | SOHA | Igen (kontrolláltan) |
| Példa | "Sikertelen bejelentkezés. Ellenőrizd az adataid." | AuthError: Invalid password hash comparison for user_id=1423, ip=82.0.1.5 |
Jó felhasználói hibaüzenet ismérvei:
- Konkrét – mit csinált rosszul vagy mi történt
- Actionable – mit tehet a felhasználó ezután
- Nem ijesztő – nincs technikai zsargon
- Nem hazug – ne írd, hogy "sikeres", ha nem az volt
ROSSZ: "500 Internal Server Error"
ROSSZ: "NullPointerException at line 423"
ROSSZ: "Hiba történt" (mit? miért? mi a teendő?)
JÓ: "A feltöltés sikertelen. A fájl mérete meghaladja az 5 MB-os limitet."
JÓ: "Nem sikerült kapcsolódni. Ellenőrizd az internet kapcsolatot, majd próbáld újra."
JÓ: "Ez az email cím már regisztrálva van. Bejelentkezési kísérlet?"
4. Egyedi exception osztályok (hibaosztályok)
Ahogy a kódbázis növekszik, érdemes saját exception osztályokat létrehozni. Ez két okból hasznos:
- Szándék jelzése –
PaymentDeclinedExceptionsokkal informatívabb, mintException - Szelektív elkapás – el tudod különíteni, mi az üzleti hiba és mi az infrastruktúra hiba
# Hibaosztály hierarchia
class AppException(Exception):
"""Minden alkalmazás-specifikus hiba ősosztálya"""
pass
class UserFacingError(AppException):
"""Hibák, amelyeket a felhasználónak mutathatunk"""
pass
class ServiceUnavailableError(AppException):
"""Infrastruktúra hibák - a felhasználónak csak annyit mondunk: próbáld újra"""
pass
class ValidationError(UserFacingError):
"""Bemeneti adat validációs hibák"""
def __init__(self, field: str, message: str):
self.field = field
super().__init__(message)
// JavaScript/Node.js változat
class AppError extends Error {
constructor(message, statusCode = 500, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational; // ismert hiba vagy váratlan programhiba?
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} nem található`, 404);
}
}
Az isOperational flag egy hasznos minta: megkülönbözted a várt üzleti hibákat (rossz input, nem létező erőforrás) a váratlan programhibáktól (null pointer, tömb index túllépés). Az utóbbira más a reakció: nem csak logolsz, hanem valószínűleg alertet küldesz a csapatnak.
5. Graceful degradation – amikor a rendszer egy része meghibásodik
A graceful degradation azt jelenti: ha egy rész nem működik, a többi még igen.
Képzeld el egy e-commerce oldalt. Ha a termékajánlások API-ja lassú vagy leáll:
ROSSZ viselkedés: Az egész terméklista oldal fehér lapot dob
JÓ viselkedés: A terméklista betölt, az ajánlások helyén "Feltöltés folyamatban..."
// Graceful degradation - ha az ajánlások nem érhetők el, az oldal működik nélkülük
async function getProductPage(productId) {
const product = await getProduct(productId); // kötelező, nélküle nincs oldal
let recommendations = [];
try {
recommendations = await getRecommendations(productId);
} catch (error) {
// Az ajánlások opcionálisak - ha megbuknak, üres listával folytatunk
logger.warn(`Recommendations unavailable for product ${productId}: ${error.message}`);
}
return { product, recommendations };
}
Ez a minta különösen fontos, ha külső szolgáltatásokra (third-party API-k, microservice-ek) hivatkozol. Soha ne feltételezd, hogy azok mindig elérhetők.
6. Összefoglalás – A production-ready hibakezelés receptje
- Sose engedd ki a stack trace-t production-ban – logold el, de a felhasználónak ne mutasd
- Rétegeld a hibakezelést – specifikus helyen specifikus hiba, globális handler a többinek
- Minden hibánál naplózz – legalább ERROR szinten, a stack trace-el együtt
- Felhasználói üzenet ≠ fejlesztői üzenet – írj mindkettőt külön
- Egyedi exception osztályok – segítenek szándékot jelezni és szelektíven elkapni
- Graceful degradation – ha egy rész megbukik, a többi fusson tovább
Életszerű példák
1. eset: Bejelentkezési hiba
Egy junior fejlesztő ezt írta:
// ROSSZ
public function login(Request $request) {
$user = User::where('email', $request->email)->firstOrFail();
// Ha nem találja a felhasználót: "No query results for model [User]"
// Ez a Laravel belső hibaüzenete kerül ki a válaszba!
if (!Hash::check($request->password, $user->password)) {
throw new Exception("Wrong password for user ID: " . $user->id);
// A user ID-t és hogy "wrong password" – ezt sem kellene kiküldeni!
}
}
Helyes verzió:
// JÓ
public function login(Request $request) {
$user = User::where('email', $request->email)->first(); // firstOrFail() helyett
if (!$user || !Hash::check($request->password, $user->password)) {
// Szándékosan ugyanaz az üzenet - ne áruljuk el, melyik adat volt hibás
return response()->json([
'error' => 'Helytelen email cím vagy jelszó.'
], 401);
}
// Logolás - de csak a szükséges adatokat, nem a jelszót!
Log::info('Successful login', ['user_id' => $user->id, 'ip' => $request->ip()]);
return response()->json(['token' => $user->createToken('auth')->plainTextToken]);
}
Miért fontos, hogy "Helytelen email cím vagy jelszó" üzenetet adjunk? Ha külön jelezzük, hogy az email nem létezik vs. jelszó rossz – a támadónak segítünk leellenőrizni, melyik emailek léteznek a rendszerben (account enumeration).
2. eset: Külső API hívás meghibásodása
Egy fizetési integráció, ahol a külső gateway időnként leáll:
// ROSSZ - nincs hibakezelés, az egész kérés elszáll
async function processPayment(userId, amount) {
const result = await stripeClient.charges.create({
amount: amount * 100,
currency: 'huf',
customer: userId
});
return result;
}
// JÓ - differenciált hibakezelés
async function processPayment(userId, amount) {
try {
const result = await stripeClient.charges.create({
amount: amount * 100,
currency: 'huf',
customer: userId
});
return { success: true, chargeId: result.id };
} catch (error) {
if (error.type === 'StripeCardError') {
// Felhasználói hiba - a kártya volt a probléma
logger.warn(`Card declined for user ${userId}: ${error.code}`);
return {
success: false,
userMessage: 'A kártya elutasítva. Ellenőrizd a kártyaadataid.'
};
}
if (error.type === 'StripeConnectionError' || error.type === 'StripeAPIError') {
// Infrastruktúra hiba - a Stripe volt a probléma
logger.error(`Stripe API error for user ${userId}`, {
errorType: error.type,
message: error.message
});
return {
success: false,
userMessage: 'A fizetési rendszer átmenetileg nem elérhető. Próbáld újra pár perc múlva.'
};
}
// Váratlan hiba
logger.error(`Unexpected payment error for user ${userId}`, { error });
throw error; // A globális handler kapja el
}
}
3. eset: Form validáció hibakezelése
# ROSSZ - egy generikus hibaüzenet mindenre
@app.route('/register', methods=['POST'])
def register():
try:
create_user(request.json)
return jsonify({"message": "OK"})
except Exception as e:
return jsonify({"error": str(e)}), 400 # A belső hiba szövege kimegy!
# JÓ - specifikus, mezőhöz kötött hibaüzenetek
@app.route('/register', methods=['POST'])
def register():
data = request.json
errors = {}
if not data.get('email') or '@' not in data['email']:
errors['email'] = 'Érvényes email cím szükséges.'
if not data.get('password') or len(data['password']) < 8:
errors['password'] = 'A jelszónak legalább 8 karakter hosszúnak kell lennie.'
if errors:
return jsonify({"errors": errors}), 422
try:
user = create_user(data)
return jsonify({"userId": user.id}), 201
except DuplicateEmailError:
return jsonify({"errors": {"email": "Ez az email cím már foglalt."}}), 409
except Exception:
logger.exception("Unexpected error during user registration")
return jsonify({"error": "Regisztráció sikertelen. Kérjük, próbáld újra."}), 500
Tesztfeladatok
1. feladat – Felismerés
Melyik hibaüzenet megfelelő production környezetben a felhasználó számára?
a) "Database connection failed: ECONNREFUSED 127.0.0.1:5432"
b) "A szolgáltatás átmenetileg nem elérhető. Próbáld újra néhány perc múlva."
c) "Error: null pointer exception at UserService.java:234"
d) "SQL Error: Table 'users' doesn't exist"
2. feladat – Try-catch elhelyezés
Az alábbi kódrészletben mi a probléma a hibakezeléssel?
def save_user_profile(user_id, profile_data):
try:
validate_profile(profile_data)
user = db.get_user(user_id)
user.update(profile_data)
db.save(user)
send_confirmation_email(user.email)
invalidate_cache(f"user:{user_id}")
except Exception:
pass
a) Nincs probléma, ez a helyes megközelítés
b) Az except Exception: pass elnyelni minden hibát, és semmit nem naplóz
c) A try-catch túl szűk, csak a db.save() hívást kéne körbevenni
d) A validáció nem kerülhet try-catch-be
3. feladat – Graceful degradation
Egy webalkalmazásban a főoldal betöltésekor egyszerre töltödik be: a felhasználó profilja, a legutóbbi rendelések listája és a termékajánlások. Ha a termékajánlások API-ja 5 másodperces timeout után hibát dob, mi a helyes viselkedés?
a) Az egész főoldal 500-as hibát dob
b) A főoldal betölt, de a termékajánlások helyén egy semleges "Ajánlataink hamarosan" üzenet jelenik meg
c) A felhasználó logout-olódik és újra kell bejelentkeznie
d) A szerver 30 másodpercig vár, hátha az API feléled
4. feladat – Hibaüzenet írása
Egy felhasználó olyan fájlt próbál feltölteni, amely 15 MB méretű, de a limit 5 MB. Az aktuális hibaüzenet: "413 Request Entity Too Large". Írj egy jobb felhasználói hibaüzenetet (max 2 mondat)!
(Nyílt végű feladat – nincs egyetlen helyes válasz, de legyen konkrét és actionable.)
5. feladat – Bejelentkezési hiba
Miért helytelen ez a hibaüzenet bejelentkezéskor: "A megadott email cím nem regisztrált."?
a) Nem magyar
b) Segíti a támadókat abban, hogy azonosítsák, melyik email cím létezik a rendszerben
c) Túl rövid, részletesebben kellene leírni a hibát
d) Nincs probléma, ez egy megfelelő hibaüzenet
6. feladat – Egyedi exception osztályok
Mire való egy egyedi exception osztály (pl. PaymentDeclinedException) egy általános Exception helyett?
a) Teljesítményt javít, mert gyorsabb elkapni
b) Lehetővé teszi a szelektív elkapást és jelzi a hiba szándékolt típusát
c) Kötelező minden production kódban
d) Csak dokumentációs célra jó, funkcionálisan nincs különbség
7. feladat – Log vs. felhasználói üzenet
Melyik információt szabad a log fájlba írni, de a felhasználónak megjeleníteni NEM szabad?
a) A hiba dátuma és ideje
b) A felhasználó által megadott beviteli mező neve
c) A belső adatbázis tábla neve, ahol a hiba keletkezett
d) Az HTTP státuszkód
8. feladat – Kódértékelés
Értékeld az alábbi Node.js kódrészletet! Mi a fő probléma?
app.get('/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
a) Nincs probléma
b) error.message tartalmazhat belső adatot, amit nem kéne kiküldeni
c) A try-catch felesleges, async kódban nem kell
d) A státuszkód mindig 500 legyen
9. feladat – Hibakezelési rétegek
Egy webalkalmazásban hol érdemes globális, "catch-all" hibakezelőt elhelyezni?
a) Minden egyes függvényen belül
b) Csak az adatbázis hívások körül
c) Middleware/framework szinten, ahol minden nem kezelt exception-t elnap és logol
d) A frontend oldalon, JavaScript-ben
10. feladat – Feladatszituáció
Az alábbi függvény egy külső időjárás API-t hív meg, és a result-ot visszaadja a frontendnek. Írj hibakezelést hozzá (bármely általad ismert nyelven vagy pszeudokódban), amely:
- Ha az API nem érhető el (timeout / connection error): visszaad egy üres objektumot és WARNING szinten logol
- Ha az API érvénytelen adatot ad vissza (parse error): ERROR szinten logol és kivételt dob
# Kiindulás:
def get_weather(city: str) -> dict:
response = requests.get(f"https://api.weather.example.com/v1/{city}")
return response.json()
(Nyílt végű feladat – a megoldást össze lehet hasonlítani a fejezet mintáival.)