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 দেখায়:
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"])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'), তার ঠিক উপরে কোন লাইনে ঘটেছে, তারপর কে কাকে ডেকেছিল সেই শিকল।
except সেটা ধরে।#try / except / else / finally
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 ধরো, সব না
# ❌ খারাপ: সব কিছু চুপচাপ গিলে ফেলা
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 এর অর্থ পরিষ্কার হয়, আর এক জায়গায় সব সামলানো যায়:
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: আসল কারণ হারিয়ো না
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): করে ফেলো, সমস্যা হলে সামলাও
# 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 এ পাঠানো যায় না
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)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 করো।
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 ভরে যাবে।