Error Handling ও Logging

try/except, নিজের exception, logging এর নিয়ম

রোডম্যাপের মাস ২পড়তে প্রায় ১৬ মিনিট

Production এ সব কিছু ভাঙে: database এর সংযোগ কেটে যায়, payment gateway সাড়া দেয় না, user অদ্ভুত data পাঠায়, disk ভরে যায়। ভালো programmer এর code ভাঙে না এমন না, বরং ভাঙলে ঠিকভাবে ভাঙে: পরিষ্কার error message দেয়, অর্ধেক কাজ করে data নষ্ট করে না, আর log এ যথেষ্ট তথ্য রেখে যায় যাতে পরে কারণ খুঁজে পাওয়া যায়। এই অধ্যায় সেই দক্ষতা নিয়ে।

#Exception কী

কোনো সমস্যা হলে Python একটা exception "ছুঁড়ে" (raise করে)। কেউ সেটা "ধরে" (catch) না করলে program থেমে যায় আর traceback দেখায়:

Python
def get_price(products: dict[str, int], name: str) -> int:
    return products[name]

def checkout(cart: list[str]) -> int:
    products = {"burger": 250, "coke": 40}
    return sum(get_price(products, item) for item in cart)

checkout(["burger", "pizza"])
Text
Traceback (most recent call last):
  File "shop.py", line 8, in <module>
    checkout(["burger", "pizza"])
  File "shop.py", line 6, in checkout
    return sum(get_price(products, item) for item in cart)
  File "shop.py", line 2, in get_price
    return products[name]
KeyError: 'pizza'

Traceback নিচ থেকে উপরে পড়ো। সবচেয়ে নিচে আসল error (KeyError: 'pizza'), তার ঠিক উপরে কোন লাইনে ঘটেছে, তারপর কে কাকে ডেকেছিল সেই শিকল।

main / <module> checkout() get_price() → KeyError প্রতিটা ধাপে Python দেখে: এখানে কি try/except আছে? না থাকলে আরও উপরে যায়। কেউ না ধরলে program থামে, traceback দেখায়
Exception যেখানে ঘটে সেখান থেকে call stack বেয়ে উপরে ওঠে, যতক্ষণ না কোনো except সেটা ধরে।

#try / except / else / finally

Python
def read_age(raw: str) -> int | None:
    try:
        age = int(raw)                    # এখানে ValueError হতে পারে
    except ValueError:
        print(f"'{raw}' কোনো সংখ্যা না")
        return None
    else:
        # try তে কোনো error না হলে চলে
        print("সংখ্যা পাওয়া গেছে")
        return age
    finally:
        # সবসময় চলে, error হোক বা না হোক (cleanup এর জন্য)
        print("যাচাই শেষ")

#নির্দিষ্ট exception ধরো, সব না

Python
# ❌ খারাপ: সব কিছু চুপচাপ গিলে ফেলা
try:
    process_payment(order)
except Exception:
    pass                     # কী হলো কেউ জানবে না। টাকা কাটলো কিনা? 😱

# ✓ ভালো: যেটা আশা করছো আর সামলাতে পারো, শুধু সেটা ধরো
try:
    process_payment(order)
except PaymentDeclinedError as e:
    notify_user(order.user, f"Payment হয়নি: {e.reason}")
except GatewayTimeoutError:
    schedule_retry(order)
# অন্য অপ্রত্যাশিত error উপরে যাক, যাতে log হয় আর আমরা জানতে পারি

#কিছু সাধারণ built-in exception

Exception কখন
ValueError Type ঠিক, কিন্তু value ভুল (int("abc"))
TypeError ভুল type ("5" + 5)
KeyError dict এ key নেই
IndexError list এর সীমার বাইরে
AttributeError Object এ এই attribute নেই (প্রায়ই None এর উপর কিছু ডাকলে)
FileNotFoundError File নেই
TimeoutError, ConnectionError Network সমস্যা
ZeroDivisionError শূন্য দিয়ে ভাগ

#নিজের exception বানানো

বড় app এ নিজের exception এর একটা পরিবার রাখা ভালো। এতে error এর অর্থ পরিষ্কার হয়, আর এক জায়গায় সব সামলানো যায়:

Python
class AppError(Exception):
    """আমাদের app এর সব error এর মূল।"""

class NotFoundError(AppError):
    def __init__(self, resource: str, resource_id: int) -> None:
        self.resource = resource
        self.resource_id = resource_id
        super().__init__(f"{resource} #{resource_id} পাওয়া যায়নি")

class InsufficientBalanceError(AppError):
    def __init__(self, needed: int, available: int) -> None:
        self.needed = needed
        self.available = available
        super().__init__(f"দরকার {needed}, আছে {available}")

class PermissionDeniedError(AppError):
    pass


def withdraw(balance: int, amount: int) -> int:
    if amount > balance:
        raise InsufficientBalanceError(needed=amount, available=balance)
    return balance - amount

পরে FastAPI তে এক জায়গায় বলে দেবো: NotFoundError হলে 404, PermissionDeniedError হলে 403, InsufficientBalanceError হলে 400। প্রতিটা route এ আলাদা করে লিখতে হবে না। (Error handling in API)

#Exception chaining: আসল কারণ হারিয়ো না

Python
import httpx

class PaymentGatewayError(AppError):
    pass

def charge(amount: int) -> dict:
    try:
        r = httpx.post("https://gateway.example/charge", json={"amount": amount}, timeout=10)
        r.raise_for_status()
        return r.json()
    except httpx.HTTPError as e:
        raise PaymentGatewayError("Payment gateway তে সমস্যা") from e   # from e!

from e দিলে traceback এ দুটোই দেখা যাবে: আমাদের পরিষ্কার message আর তার পেছনের আসল network error। না দিলে debug করা অনেক কঠিন।

#EAFP: Python এর ধরন

দুইটা দর্শন আছে:

  • LBYL (Look Before You Leap): আগে check করো, তারপর করো
  • EAFP (Easier to Ask Forgiveness than Permission): করে ফেলো, সমস্যা হলে সামলাও
Python
# LBYL
if "email" in data and data["email"] is not None:
    send(data["email"])

# EAFP (Python এ বেশি প্রচলিত)
try:
    send(data["email"])
except KeyError:
    log_missing_email()

File এর ক্ষেত্রে EAFP আরও নিরাপদ: if path.exists(): open(path) এর মাঝখানে অন্য কেউ file মুছে ফেলতে পারে। সরাসরি খুলে FileNotFoundError ধরা নির্ভরযোগ্য।

#Logging: print এর বদলে

print development এ চলে, কিন্তু production এ:

  • কোনটা সাধারণ তথ্য আর কোনটা জরুরি সমস্যা, বোঝা যায় না
  • কখন ঘটলো, কোন file থেকে, জানা যায় না
  • বন্ধ করা বা file/monitoring service এ পাঠানো যায় না
Python
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
)
logger = logging.getLogger(__name__)      # প্রতিটা module এ নিজের logger


def place_order(order_id: int, amount: int) -> None:
    logger.info("Order শুরু: id=%s amount=%s", order_id, amount)
    try:
        charge(amount)
    except PaymentGatewayError:
        logger.exception("Payment ব্যর্থ: order_id=%s", order_id)   # traceback সহ
        raise
    logger.info("Order সম্পন্ন: id=%s", order_id)
Text
2026-10-29 10:15:02,114 INFO [orders] Order শুরু: id=42 amount=55000
2026-10-29 10:15:12,301 ERROR [orders] Payment ব্যর্থ: order_id=42
Traceback (most recent call last):
  ...

#Log level

Level কখন
DEBUG খুঁটিনাটি, শুধু development এ (query result: ...)
INFO স্বাভাবিক গুরুত্বপূর্ণ ঘটনা (order হলো, user login করলো)
WARNING অস্বাভাবিক কিন্তু চলছে (retry করতে হলো, disk ৮০% ভরা)
ERROR কাজটা ব্যর্থ হলো (payment fail)
CRITICAL পুরো system বিপদে (database এ একদম সংযোগ নেই)

Production এ সাধারণত INFO বা WARNING থেকে উপরের log রাখা হয়।

#Error সামলানোর জায়গা: সীমানায়

প্রতিটা function এ try/except লাগিয়ো না। নিয়ম হলো:

  • নিচের স্তরে (database, external API): নির্দিষ্ট error কে তোমার app এর অর্থপূর্ণ error এ বদলাও (httpx.HTTPError → PaymentGatewayError)
  • মাঝের স্তরে (business logic): সাধারণত ধরবে না, উপরে যেতে দাও। শুধু তখন ধরো যখন সত্যিই কিছু করতে পারো (retry, বিকল্প পথ)।
  • সবচেয়ে উপরের স্তরে (API handler, CLI এর main): সব error ধরে user কে পরিষ্কার message দাও আর log করো।
Python
def main() -> None:
    try:
        run_app()
    except AppError as e:
        print(f"সমস্যা: {e}")          # user এর জন্য বোঝার মতো message
        raise SystemExit(1)
    except Exception:
        logger.exception("অপ্রত্যাশিত error")
        print("অপ্রত্যাশিত সমস্যা হয়েছে, log দেখুন")
        raise SystemExit(2)

#অনুশীলন

#নিজেকে যাচাই করো

Traceback কোন দিক থেকে পড়বে?

নিচ থেকে উপরে। সবচেয়ে নিচের লাইনে আসল error এর ধরন আর message, তার উপরে কোন file এর কোন লাইনে ঘটেছে, তারপর কে কাকে ডেকেছিল।

`except Exception: pass` কেন বিপজ্জনক?

সব error চুপচাপ গিলে ফেলে। Bug লুকিয়ে থাকে, system ভুল অবস্থায় চলতে থাকে (যেমন টাকা কাটলো কিন্তু order হলো না), আর কেউ জানতে পারে না।

`raise NewError(...) from e` এর `from e` কেন দরকারি?

এটা নতুন error এর সাথে আসল কারণ (e) জুড়ে দেয়। Traceback এ দুটোই দেখা যায়, ফলে বোঝা যায় নিচে আসলে কী ঘটেছিল।

`logger.error()` আর `logger.exception()` এর পার্থক্য কী?

দুটোই ERROR level এ লেখে, কিন্তু logger.exception() সাথে পুরো traceback যোগ করে। এটা except block এর ভেতরে ব্যবহার করতে হয়।

কোন স্তরে error ধরে user কে message দেখানো উচিত?

সবচেয়ে উপরের স্তরে, যেমন API handler বা CLI এর main function। নিচের স্তরগুলো error কে অর্থপূর্ণ নামে বদলে উপরে পাঠাবে, আর মাঝখানে শুধু তখনই ধরবে যখন সত্যিই কিছু করার আছে (যেমন retry)।

চিহ্ন দিলে sidebar আর রোডম্যাপে এই অধ্যায়ের station ভরে যাবে।