8.3 Hibakezelés

← 8. Production-ready projekt építés

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 payments tá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 üzenetFejlesztői üzenet (log)
TartalomEmberi, érthetőTechnikai, részletes
CélTájékoztatás, következő lépésDiagnosztika, nyomozás
Stack traceSOHAMindig
Belső adatSOHAIgen (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:

  1. Konkrét – mit csinált rosszul vagy mi történt
  2. Actionable – mit tehet a felhasználó ezután
  3. Nem ijesztő – nincs technikai zsargon
  4. 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:

  1. Szándék jelzésePaymentDeclinedException sokkal informatívabb, mint Exception
  2. 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

  1. Sose engedd ki a stack trace-t production-ban – logold el, de a felhasználónak ne mutasd
  2. Rétegeld a hibakezelést – specifikus helyen specifikus hiba, globális handler a többinek
  3. Minden hibánál naplózz – legalább ERROR szinten, a stack trace-el együtt
  4. Felhasználói üzenet ≠ fejlesztői üzenet – írj mindkettőt külön
  5. Egyedi exception osztályok – segítenek szándékot jelezni és szelektíven elkapni
  6. 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.)

Scroll to Top