RuntimeError یکی از پرکاربردترین خطاهای پایتون است که مستقیماً توسط مفسر یا کد شما پرتاب می‌شود، نه از سوی سیستمعامل؛ معنایش این است که برنامه در حال اجرا به وضعیتی رسیده که از نظر منطقی مجاز نیست. برخلاف OSError که ریشه‌اش در لایهٔ هسته است، RuntimeError نشانهٔ شکستن یک قرارداد درونی برنامه است. در این مقاله، تجربه‌ام از ریشه‌یابی ده‌ها پروندهٔ واقعی این خطا را با شما به اشتراک می‌گذارم.

RuntimeError چیست و از کجا پرتاب می‌شود؟

RuntimeError یک استثنای سطح بالا در پایتون است که وقتی پرتاب می‌شود که برنامه در حال اجرا به وضعیتی رسیده که از نظر منطقی یا قراردادی قابل‌قبول نیست. این خطا معمولاً از دو منبع می‌آید: یا خود مفسر پایتون در وضعیت‌های خاص آن را پرتاب می‌کند، یا کد شما (یا کتابخانه‌ای که استفاده می‌کنید) به‌عنوان یک خطای «شرایط اجرا» آن را raise می‌کند.

ساختار ارث‌بری آن به این شکل است:

BaseException
 └── Exception
      └── RuntimeError
           ├── NotImplementedError
           ├── RecursionError
           └── (سایر زیرکلاس‌های خاص)

توجه کنید که RuntimeError در کنار OSError قرار دارد، نه زیرمجموعهٔ آن. این یعنی گرفتن except OSError هرگز خطای RuntimeError را نمی‌گیرد و برعکس. این تفکیک بنیادی، منشأ بسیاری از سردرگمی‌های توسعه‌دهندگان تازه‌کار است. برای درک جامع‌تر خانوادهٔ خطاهای پایتون، مدیریت خطا در پایتون و آموزش پایتون از صفر را پیشنهاد می‌کنم.

RuntimeError یک «دادگاه داخلی» است: نه سیستمعامل، نه دیسک، نه شبکه — بلکه خودِ منطق برنامه حکم می‌کند که این وضعیت نباید وجود داشته باشد.

نکتهٔ ظریف در تعریف: مفهوم «Runtime» در علوم کامپیوتر به فاز اجرای برنامه اشاره دارد، در مقابل فاز کامپایل یا طراحی. اصطلاح «Runtime» از نظر تاریخی در مستندات کلاسیک برنامه‌نویسی توصیف شده است و در ویکی‌پدیا هم ذیل Runtime (program lifecycle phase) توضیح داده شده است. تفاوت کلیدی با خطاهای سینتکسی این است: خطاهای سینتکسی قبل از اجرا تشخیص داده می‌شوند، ولی RuntimeError فقط وقتی ظاهر می‌شود که برنامه واقعاً در حال اجراست.

کجا RuntimeError پرتاب می‌شود؟

اگر بخواهیم فهرست کنیم، موارد زیر شایع‌ترین نقاط پرتاب RuntimeError در پایتون مدرن هستند. هر مورد، یک الگوی مشخص دارد و راه‌حل متفاوتی را می‌طلبد.

تغییر دیکشنری در حین پیمایش

کد زیر در پایتون ۳ قطعاً خطا می‌دهد:

d = {"a": 1, "b": 2, "c": 3}
for key in d:
    if key == "a":
        del d["b"]  # RuntimeError: dictionary changed size during iteration

مفسر پایتون این وضعیت را تشخیص می‌دهد و بلافاصله RuntimeError پرتاب می‌کند. راه‌حل: ابتدا کلیدها را در یک لیست کپی کنید و سپس پیمایش کنید:

for key in list(d.keys()):
    if key == "a":
        del d["b"]

تغییر مجموعه یا لیست در حین پیمایش

معادل بالا در set هم رخ می‌دهد:

s = {1, 2, 3}
for x in s:
    if x == 1:
        s.add(4)  # RuntimeError: Set changed size during iteration

تعداد نامتناسب عملوندها در تابع

در توابعی که با *args کار می‌کنند، اگر تعداد آرگومان‌ها با انتظار تابع مطابقت نداشته باشد، گاهی RuntimeError ظاهر می‌شود، مخصوصاً در کتابخانه‌های قدیمی:

# نمونه‌ای از کتابخانه‌های C-extension که تعداد آرگومان‌ها را سخت‌گیرانه چک می‌کنند

شکست در عملیات‌های threading

در پایتون، عملیات‌های خاص threading اگر در زمان نامناسب انجام شوند، RuntimeError می‌دهند:

import threading

lock = threading.Lock()

with lock:
    with lock:  # RuntimeError: cannot acquire a non-reentrant lock twice
        pass

مفهوم قفل‌های غیرقابل‌بازگشت (non-reentrant) در طراحی سیستم‌های همزمانی بسیار اساسی است و توضیح آن در ساخت API با پایتون که در بخشی با concurrency سروکار دارد، مفید است.

فراخوانی asyncio در محیط نامناسب

یکی از پرتکرارترین RuntimeErrorها در پروژه‌های async:

import asyncio

asyncio.run(main())  # در حالت عادی خوب است

# اما داخل یک حلقهٔ رویداد در حال اجرا:
async def outer():
    asyncio.run(inner())  # RuntimeError: asyncio.run() cannot be called from a running event loop

این خطا نشان می‌دهد برنامه‌نویس مفاهیم حلقهٔ رویداد را با هم مخلوط کرده است. راه‌حل: از await مستقیم استفاده کنید، نه asyncio.run.

فراخوانی event loop بسته

loop = asyncio.new_event_loop()
loop.close()
loop.run_until_complete(something())  # RuntimeError: Event loop is closed

RuntimeError در deepcopy و pickle

در بازتولید اشیای پیچیده، اگر شیء به منابع خارجی مثل فایل یا سوکت گره خورده باشد، RuntimeError ظاهر می‌شود:

import copy

# اشیایی که فایل باز دارند، معمولاً قابل deepcopy نیستند

این خطا مخصوصاً در پروژه‌های وب اسکرپینگ با پایتون رخ می‌دهد، چون sessionهای HTTP معمولاً حاوی سوکت‌های باز هستند.

تفاوت با خطاهای مشابه

برای تشخیص درست، باید RuntimeError را از خطاهای نزدیکش جدا کنید:

خطامعنامنبع
RuntimeErrorوضعیت اجرا با قرارداد برنامه ناسازگار استمفسر یا کد برنامه
ValueErrorمقدار دریافتی نوع درست دارد ولی محتوا نامعتبر استکد یا تابع stdlib
TypeErrorنوع داده اشتباه به تابع یا عملگر داده شدهمفسر پایتون
AssertionErrorادعای assert شکست خورده استکد با استفاده از assert
RecursionErrorعمق بازگشت از حد عبور کرده (زیرکلاس RuntimeError)مفسر

تفکیک RuntimeError از ValueError یکی از پرتکرارترین موارد اشتباه در کدهای تولیدی است. قاعدهٔ من ساده است: اگر مسئله در «محتوا»ست، ValueError؛ اگر در «نوع» است، TypeError؛ اگر در «وضعیت اجرای برنامه» است، RuntimeError. این تفکیک در کد زیر شفاف می‌شود:

def divide(a, b):
    if not isinstance(a, (int, float)):
        raise TypeError("a must be numeric")
    if b == 0:
        raise ValueError("b must not be zero")
    if a > 1e100:
        raise RuntimeError("operation would overflow")
    return a / b

برای درک عمیق‌تر تفاوت‌های این خطاها، مقاله‌های خطای ValueError در پایتون و خطای TypeError در پایتون را ببینید.

شش سناریوی واقعی در پروژه‌ها

در طول سال‌ها کار با پایتون، RuntimeError را در این شش الگو دیده‌ام. شناختن هر الگو، تشخیص را چند برابر سریع‌تر می‌کند.

سناریوی اول: بازگشت عمیق و RecursionError

RecursionError زیرکلاس مستقیم RuntimeError است و وقتی پرتاب می‌شود که عمق بازگشت از سقف پایتون (پیش‌فرض ۱۰۰۰) عبور کند. این خطا در الگوریتم‌های بازگشتی، پیمایش درخت و توابعی که روی داده‌های تودرتو کار می‌کنند بسیار شایع است. راه‌حل‌های عملی در خطای RecursionError در پایتون آمده است.

سناریوی دوم: مدل‌های Django در خارج از request

در آموزش Django برای مبتدیان دیدیم که کوئری روی مدل‌ها در context مناسب باید انجام شود. اگر کدی تلاش کند در یک thread جداگانه به دیتابیس دست بزند، ممکن است با RuntimeError: Database access not allowed, use the "django_db" mark, or the "db" or "transactional_db" fixtures مواجه شود که ریشهٔ آن، محدودیت‌های تست Django است.

سناریوی سوم: پرکردن فایل بسته

کدی که با context manager کار نمی‌کند و فایل را دستی می‌بندد، در فراخوانی بعدی با خطایی مواجه می‌شود که در برخی کتابخانه‌ها به‌شکل RuntimeError: I/O operation on closed file ظاهر می‌شود. الگوهای درست در کار با فایل‌ها در پایتون به‌تفصیل توضیح داده شده است.

سناریوی چهارم: threading و GIL

در پایتون، GIL (Global Interpreter Lock) هرگز به‌طور صریح در کد کاربر ظاهر نمی‌شود، ولی رفتار آن گاهی به RuntimeError منجر می‌شود. مثلاً تلاش برای اجرای دو حلقهٔ event در یک thread، یا اجرای عملیات خاصی که فقط در main thread مجاز است:

import threading
import asyncio

def worker():
    asyncio.run(main())  # در thread غیر main گاهی خطا می‌دهد

threading.Thread(target=worker).start()

سناریوی پنجم: تغییر اندازه حین پیمایش در async

در asyncio، اگر در حین async for روی یک ژنراتور، اندازه مجموعه تغییر کند، ممکن است RuntimeError ببینید. این حالت در سرویس‌های realtime که داده‌های ورودی از کانال می‌آیند بسیار رخ می‌دهد.

سناریوی ششم: محدودیت‌های C-extensions

کتابخانه‌هایی که با C یا Cython نوشته شده‌اند، گاهی در وضعیت‌های خاص RuntimeError با پیام‌های اختصاصی پرتاب می‌کنند. مثلاً PyTorch در ناسازگاری شکل تنسورها، یا OpenCV در ناسازگاری نسخه‌های کتابخانه. در این موارد، پیام خطا معمولاً صریح است و بهترین راه، جستجوی مستقیم همان پیام در مستندات کتابخانه است.

در پروژه‌های ترکیبی که با دیتابیس هم سروکار دارند، RuntimeError ممکن است در پوشش خطاهای سطح پایین‌تر ظاهر شود؛ برای درک این رابطه، اتصال پایتون به MySQL را مطالعه کنید.

RuntimeError اغلب یک «آینه» است: نشان می‌دهد معماری کد شما با فرض‌های پایتون در تضاد است، نه اینکه پایتون اشتباه می‌کند.

روش تشخیص در پنج گام

در برخورد با RuntimeError، پروتکل زیر را در پروژه‌های خودم اجرا می‌کنم و در اکثر پرونده‌ها، گام سوم یا چهارم مقصر را روشن می‌کند.

گام اول: خواندن دقیق پیام

پیام‌های RuntimeError معمولاً بسیار توصیفی‌اند. چند نمونه:

RuntimeError: dictionary changed size during iteration
RuntimeError: Event loop is closed
RuntimeError: cannot reuse already awaited coroutine
RuntimeError: Working outside of application context

هر کدام از این پیام‌ها، دقیقاً به یک الگوی مشخص اشاره دارد. اولین قدم، جستجوی مستقیم همان پیام در مستندات رسمی یا GitHub Issues است — بدون ترجمه به فارسی. جستجوی انگلیسی، شما را سریع‌تر به پاسخ می‌رساند.

گام دوم: بررسی traceback کامل

Traceback در پایتون از پایین به بالا خوانده می‌شود. آخرین فریم، جایی است که خطا پرتاب شده. اولین فریم، جایی است که کل زنجیره شروع شده. اگر بین این دو، فریم‌هایی از کتابخانه‌های ثالث می‌بینید، تشخیص درست این است که «خطای شما» از ترکیب کد شما و کتابخانه ناشی می‌شود:

import traceback

try:
    risky_operation()
except RuntimeError:
    traceback.print_exc(limit=None, chain=True)

پارامتر chain=True زنجیرهٔ استثناهای وابسته (در صورت استفاده از raise from) را نشان می‌دهد.

گام سوم: بازتولید در محیط کنترل‌شده

پیش از هر تغییری، خطا را در حداقل کد ممکن بازتولید کنید. اگر خطا در محیط async رخ می‌دهد ولی در کد همزمان نه، احتمالاً مسئله در interaction حلقهٔ رویداد و کد همزمان است. اگر خطا فقط در محیط چندریسمانی رخ می‌دهد، احتمالاً مسئله در اشتراک حالت (state) بین threadهاست.

گام چهارم: شرط‌گذاری موقت

در وضعیت‌های سخت، می‌توان با اضافه‌کردن شرط‌های موقت و لاگ، دقیقاً لحظهٔ وقوع خطا را شکار کرد:

import logging

def guarded_iterate(data):
    size = len(data)
    for i, item in enumerate(data):
        if len(data) != size:
            logging.error("collection size changed at index %s: %s -> %s", i, size, len(data))
            raise RuntimeError("concurrent modification detected")
        yield item

این تکنیک را در پروژه‌های چندنخی زیاد استفاده کرده‌ام؛ چون پایتون خودش خطای تغییر اندازه را می‌دهد ولی نمی‌گوید کجا و چرا.

گام پنجم: جداسازی thread و event loop

در برنامه‌های پیچیده، تشخیص اینکه خطا در کدام thread رخ داده، حیاتی است:

import threading
import logging

logging.info("current thread: %s", threading.current_thread().name)
logging.info("main thread: %s", threading.main_thread().name)

بسیاری از RuntimeErrorهای مربوط به asyncio، ریشه‌شان در اجرای کد async در thread غیر main است. یک نگاه به نام thread، معمولاً این فرضیه را تأیید یا رد می‌کند.

اگر خطا در لایه‌های پایین‌تر سیستم رخ دهد، تفکیک آن از OSError اهمیت دارد؛ مقالهٔ خطای OSError در پایتون این مرز را روشن می‌کند.

RuntimeError سفارشی: کجا از آن استفاده کنیم؟

RuntimeError نه‌فقط یک خطای آماده، بلکه یک الگوی طراحی است. من در پروژه‌های خودم از آن برای بیان «وضعیت‌های غیرمنتظره» استفاده می‌کنم که در آن‌ها هیچ‌کدام از خطاهای دیگر معنای دقیق ندارند. مثال:

class CircuitBreakerOpen(RuntimeError):
    """سرویس خارجی در حالت cut-off است."""

class StateMachineError(RuntimeError):
    """گذار از state A به B مجاز نیست."""

class InvariantViolation(RuntimeError):
    """یک invariant داخلی نقض شده است."""

مزیت این رویکرد: در بلوک except، می‌توانید بین خطاهای درونی برنامه و خطاهای بیرونی (مثل شبکه و دیسک) تفکیک قائل شوید. در مقابل، زیاده‌روی در استفاده از RuntimeError برای همه‌چیز، یک ضدالگو است. اگر خطای شما «مقدار نامعتبر» است، ValueError بدهید؛ اگر «نوع نامعتبر» است، TypeError.

قاعدهٔ من در طراحی خطاها: هر خطای سفارشی باید پاسخ دهد که «چه چیزی باید متفاوت باشد تا این وضعیت پیش نیاید؟». اگر پاسخ روشن است، احتمالاً خطای دقیق‌تری وجود دارد. اگر پاسخ «هیچ‌چیز، این وضعیت نباید رخ دهد» است، RuntimeError انتخاب درستی است.

الگوهای رفع امن

راه‌حل هر RuntimeError به ریشهٔ آن بستگی دارد، ولی الگوهای کلی زیر در بیشتر پرونده‌ها مفیدند.

الگوی اول: کپی کردن قبل از پیمایش

# غلط
for k in my_dict:
    if condition(k):
        del my_dict[k]

# درست
for k in list(my_dict.keys()):
    if condition(k):
        del my_dict[k]

هزینهٔ حافظه‌ای کپی در مجموعه‌های بزرگ قابل‌توجه است، ولی در عمل این کار همیشه ارزان‌تر از یک باگ تولیدی است.

الگوی دوم: بازنویسی منطق به‌جای تغییر داده

اگر متوجه شدید مکرراً در حین پیمایش، داده را تغییر می‌دهید، احتمالاً منطق شما نادرست است. الگوی درست: یک لیست جدید بسازید و در انتها جایگزین کنید:

new_dict = {k: v for k, v in my_dict.items() if not condition(k)}
my_dict.clear()
my_dict.update(new_dict)

این الگو هم از نظر عملکرد بهتر است و هم از نظر وضوح، خواناتر.

الگوی سوم: مدیریت صحیح حلقهٔ رویداد

در کدهای async، هرگز asyncio.run را در داخل کد async صدا نزنید. الگوی درست:

import asyncio

async def inner():
    await asyncio.sleep(0.1)

async def outer():
    await inner()  # نه asyncio.run(inner())

asyncio.run(outer())

الگوی چهارم: مدیریت بازگشت با حلقه

برای الگوریتم‌های بازگشتی که عمق زیادی دارند، به‌جای افزایش sys.setrecursionlimit (که خطر crash مفسر را دارد)، به حلقهٔ تکراری مهاجرت کنید:

# به‌جای بازگشت
def factorial_recursive(n):
    return 1 if n <= 1 else n * factorial_recursive(n - 1)

# نسخهٔ تکراری
def factorial_iterative(n):
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

در مسائل پیمایش درخت و گراف، الگوی stack صریح، همیشه جایگزین بهتری برای بازگشت عمیق است.

الگوی پنجم: استفادهٔ درست از lock

اگر به بازگشت در قفل نیاز دارید، به‌جای Lock از RLock استفاده کنید:

import threading

lock = threading.RLock()

with lock:
    with lock:
        pass  # این بار خطا نمی‌دهد

الگوی ششم: fail-fast با پیام‌های توصیفی

اگر در کد خودتان RuntimeError پرتاب می‌کنید، پیام را با تمام context لازم بنویسید:

if state not in VALID_TRANSITIONS[current]:
    raise RuntimeError(
        f"invalid transition: {current!r} -> {state!r}; "
        f"allowed: {sorted(VALID_TRANSITIONS[current])!r}"
    )

پیامی که خودش راه‌حل را بگوید، هزاران ساعت دیباگ در آینده صرفه‌جویی می‌کند. این رویکرد در مدیریت خطاهای PermissionError هم توصیه‌ام بود؛ خطای PermissionError در پایتون نمونه‌های بیشتری دارد.

RuntimeError در asyncio و thread

محیط‌های asyncio یکی از پربارترین سرزمین‌های RuntimeError در پایتون مدرن است. دلایل این حجم، سادگی ظاهری async است که مفاهیم عمیقی را پنهان می‌کند.

خطای «coroutine was never awaited»

این خطا گاهی به‌شکل RuntimeWarning ظاهر می‌شود ولی در نسخه‌های سخت‌گیرانه‌تر، به RuntimeError تبدیل می‌شود:

async def fetch():
    ...

# غلط
fetch()  # هیچ‌وقت اجرا نمی‌شود

# درست
await fetch()

این خطا مخصوصاً وقتی رخ می‌دهد که تابع async را در یک نقطه می‌سازید و در نقطه‌ای دیگر await می‌کنید؛ ولی در نقطهٔ دوم، ابجکت coroutine دیگر معتبر نیست.

«Event loop is closed»

این خطا در سرویس‌های long-running بسیار شایع است. وقتی asyncio.run() تمام می‌شود، حلقهٔ رویداد بسته می‌شود. اگر کدی بعد از این، تلاش کند از حلقهٔ قدیمی استفاده کند، با خطا مواجه می‌شود. راه‌حل در سرویس‌های دائمی، استفاده از loop.run_forever() به‌جای asyncio.run() است.

«Task attached to a different loop»

وقتی تسکی در یک حلقه ساخته می‌شود و در حلقهٔ دیگری await می‌شود، این خطا ظاهر می‌شود. این مسئله در تست‌های pytest-asyncio هم دیده می‌شود؛ چون هر تست، حلقهٔ رویداد خودش را دارد.

در پروژه‌های ترکیبی async و thread، رفتار RuntimeError می‌تواند با خطاهای سیستمعامل قاطی شود؛ برای تفکیک دقیق‌تر، خطای ConnectionError در پایتون مرجع مکمل خوبی است.

RuntimeError در Django و Flask

در چارچوب‌های وب، RuntimeError معنای اختصاصی پیدا می‌کند. تفکیک این خطاها، کلید نگهداری سرویس‌های تولیدی است.

«Working outside of application context» در Flask

خطای کلاسیک Flask: کدی که خارج از request به current_app یا request دسترسی پیدا می‌کند. راه‌حل:

from flask import current_app

with current_app.app_context():
    # کد درون این بلوک، به context دسترسی دارد
    ...

این خطا مخصوصاً در اسکریپت‌های CLI، workerهای Celery و کدهای راه‌اندازی رخ می‌دهد.

«Apps aren't loaded yet» در Django

در Django، اگر کدی قبل از django.setup() به مدل‌ها دسترسی پیدا کند، این خطا می‌آید. راه‌حل: django.setup() را در ابتدای اسکریپت‌های standalone صدا بزنید.

«Model class doesn't declare an explicit app_label»

وقتی مدلی بدون app_label صریح import شود، این خطا ظاهر می‌شود. راه‌حل: تنظیم DJANGO_SETTINGS_MODULE و فراخوانی django.setup() قبل از import مدل‌ها.

RuntimeError در Celery و workerها

کارگرهای Celery در فرآیند جداگانه اجرا می‌شوند. اگر کد شما در worker تلاش کند به context درخواست دسترسی پیدا کند، RuntimeError می‌گیرد. راه‌حل درست، طراحی تسک‌هایی است که به context وابسته نباشند.

پرسش‌های پرتکرار درباره RuntimeError

این بخش، پرسش‌هایی را پوشش می‌دهد که در جلسات مشاوره و انجمن‌های فنی بیشترین تکرار را داشته‌اند. پاسخ‌ها برای جستجوهای مستقیم و دستیارهای هوش مصنوعی به‌عنوان پاسخ معتبر قابل استخراج هستند.

RuntimeError با ValueError چه تفاوتی دارد؟

ValueError یعنی مقدار داده‌شده از نظر محتوا نامعتبر است، در حالی که RuntimeError یعنی وضعیت اجرای برنامه با قرارداد منطقی ناسازگار است. مثال: تابعی که عدد منفی را رد می‌کند، ValueError می‌دهد؛ ولی اگر همان تابع قبل از اجرا، وضعیت داخلی‌اش خراب باشد، RuntimeError می‌دهد.

آیا می‌توان همه RuntimeErrorها را با یک except گرفت؟

فنی ممکن است، ولی توصیه نمی‌شود. چون RecursionError و NotImplementedError زیرکلاس‌های RuntimeError هستند و رفتار متفاوتی دارند. بهتر است هر کدام را جداگانه مدیریت کنید.

چرا «dictionary changed size during iteration» این‌قدر رایج است؟

چون بیشتر توسعه‌دهندگان عادت دارند در حین پیمایش، ساختار داده را تغییر دهند. راه‌حل ساده: قبل از پیمایش، یک snapshot از کلیدها یا مقادیر بگیرید. این الگو در پروژه‌های کوچک هزینه‌ای ندارد و در پروژه‌های بزرگ، جلوی باگ‌های تصادفی را می‌گیرد.

آیا افزایش sys.setrecursionlimit راه‌حل درستی است؟

خیر. افزایش این مقدار، ریسک crash مفسر (segfault) را بالا می‌برد چون هر فراخوانی بازگشتی روی C-stack فضا می‌گیرد. راه‌حل درست، بازنویسی الگوریتم به حلقه یا استفاده از صریح stack است.

چرا در Jupyter Notebook بیشتر این خطا را می‌بینم؟

چون Jupyter خودش یک حلقهٔ رویداد در پس‌زمینه دارد و کد شما در همان حلقه اجرا می‌شود. اگر در سلول بعدی، asyncio.run() صدا بزنید، با RuntimeError مواجه می‌شوید. راه‌حل: در notebook از await مستقیم استفاده کنید یا از nest_asyncio بهره ببرید.

آیا RuntimeError همیشه نشانهٔ باگ است؟

نه. اگر کد شما intentionally یک precondition را چک می‌کند و در صورت نقض، RuntimeError می‌دهد، این یک رفتار سالم است. باگ زمانی است که خطا در وضعیتی رخ دهد که از نظر شما مجاز است.

چطور RuntimeError را در محیط تولید به‌درستی لاگ کنیم؟

الگوی توصیه‌شده من: در بالاترین لایه، همهٔ استثناها را با logging.exception لاگ کنید و در کنار آن، context حیاتی مثل شناسهٔ کاربر، شناسهٔ درخواست و وضعیت سیستم را هم بفرستید. متن پیام خطا به‌تنهایی کافی نیست:

import logging

logger = logging.getLogger(__name__)

def process(request_id: str, data: dict) -> None:
    try:
        heavy(data)
    except RuntimeError:
        logger.exception("runtime failure; request_id=%s; keys=%s", request_id, sorted(data.keys()))
        raise

آیا کتابخانه‌های خاصی برای مدیریت خطاهای پایتون وجود دارد؟

برای مدیریت سطح بالاتر، tenacity برای retry، structlog برای لاگ ساخت‌یافته و sentry-sdk برای رهگیری خطا مفیدند. ولی این کتابخانه‌ها جایگزین درک عمیق مکانیزم RuntimeError نمی‌شوند.

چرا RuntimeError در تست‌ها بیشتر از محیط تولید ظاهر می‌شود؟

چون ابزارهای تست مثل pytest و unittest محیط اجرایشان محدودتر است: هر تست ممکن است حلقهٔ رویداد خودش، دیتابیس جداگانه و context مستقل داشته باشد. رعایت این محدودیت‌ها، خودش یک مهارت است. بررسی خطاهای مشابه در سایر خانواده‌ها مثل خطای MemoryError در پایتون و خطای AttributeError در پایتون می‌تواند الگوهای مشترک را روشن کند.

آیا RuntimeError با خطاهای سینتکسی نسبت دارند؟

خیر، و این یک تفکیک بنیادی است. خطاهای سینتکسی (SyntaxError) قبل از اجرا و در فاز کامپایل تشخیص داده می‌شوند و حتی یک خط کد را اجرا نمی‌کنند. RuntimeError صرفاً در زمان اجرا و در وضعیت‌های خاص ظاهر می‌شود. برای درک این تفاوت، خطای SyntaxError در پایتون مرجع مکمل خوبی است.

درس‌هایی که این خطا به معماری من اضافه کرد

RuntimeError بیش از آنکه یک خطای فنی باشد، یک «قانون اساسی» در طراحی کد است. سه اصلی که پس از سال‌ها کار با آن، در معماری سرویس‌هایم رعایت می‌کنم:

نخست، از ساختارهای تغییرپذیر در پیمایش پرهیز کنید. اگر جایی از کد شما نیاز دارد در حین پیمایش یک مجموعه، آن را تغییر دهد، آن کد بوی بازطراحی می‌دهد. راه‌حل تمیزتر همیشه یک پاس‌دوم روی داده یا یک ساختار جدید است.

دوم، حلقهٔ رویداد را محترم بشمارید. در پایتون async، هر فراخوانی asyncio.run یعنی «من می‌خواهم کنترل کامل حلقه را در دست بگیرم». این تصمیم در لایهٔ بالای برنامه گرفته می‌شود و در لایه‌های پایین، فقط await مجاز است. وقتی این قاعده رعایت شود، بیشتر RuntimeErrorهای asyncio از بین می‌روند.

سوم، خطاها را با context پرتاب کنید. اگر کد شما RuntimeError می‌دهد، باید پیامش برای توسعه‌دهندهٔ سه ماه بعد قابل فهم باشد. حتی اگر خودتان هم همان توسعه‌دهنده باشید، این context حیاتی است. یک قاعده‌ی سرانگشتی: هر RuntimeError باید حداقل شامل state فعلی، state موردانتظار و مقادیر مرتبط باشد.

در پایان، اگر در پروژه‌ای با حالت خاصی از RuntimeError برخورد کردید که اینجا پوشش داده نشده — مثلاً در ترکیب با PyTorch، multiprocessing یا Jupyter با نسخه‌های خاص — تجربه‌تان را در دیدگاه‌ها بنویسید. به‌ویژه اگر راه‌حلی پیدا کرده‌اید که با رویکردهای معمول متفاوت است و می‌تواند برای خوانندهٔ بعدی ارزشمند باشد. 🧩