> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# قراءة ال Stack Trace وتصحيح الأخطاء Debugging للمبتدئين
- URL: https://arabroot.io/articles/قراءة-رسالة-الخطا-stack-trace-والتصحيح-debugging/
- Published: 2026-10-30T22:22:53.000Z
- Updated: 2026-10-30T23:22:04.000Z
- Description: كيف تقرأ رسالة الخطأ في Python وJavaScript سطراً بسطر، ومتى تستخدم الطباعة أو نقاط التوقف في VS Code، وكيف يجد git bisect التعديل الذي أدخل الخطأ.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة

تكتب برنامجك الأول الذي يقرأ ملفاً صغيراً ويحسب منه شيئاً، وتشغله وأنت متأكد أنه سوف يعمل، فتمتلئ الطرفية Terminal بسطور حمراء أو رمادية فيها أسماء ملفات وأرقام أسطر وكلمة `Traceback` أو `TypeError`، وآخرها جملة إنجليزية لا تبدو مفهومة. وأغلب المبتدئين يفعلون في هذه اللحظة واحداً من ثلاثة أشياء: يغلقون الطرفية ويغيرون أي سطر يشكون فيه ثم يشغلون البرنامج من جديد، أو ينسخون الرسالة كلها إلى محرك البحث، أو يكتبون في مجموعة المبرمجين «البرنامج لا يعمل، ساعدوني». والحقيقة أن هذه السطور هي أفضل مساعدة ستحصل عليها، لأنها تقول لك ما نوع الخطأ، وفي أي ملف، وفي أي سطر، ومن أي طريق وصل البرنامج إلى هذا السطر، وكل ما تحتاجه هو أن تعرف ترتيب قراءتها.

والأصعب من الرسالة الحمراء هو البرنامج الذي لا يطبع أي خطأ ولكنه يعطي نتيجة خاطئة، فيقول لك إن مجموع مشتريات عميل هو 80 وأنت تعرف أنه 200، وهنا لا توجد رسالة تقرؤها، وإنما عليك أن تبحث بنفسك عن المكان الذي انحرف فيه البرنامج. وهذا العمل كله، من قراءة رسالة الخطأ حتى إيجاد السبب وإصلاحه، اسمه التصحيح Debugging، وهو مهارة تتعلمها كما تتعلم الكتابة، وليس موهبة يولد بها بعض المبرمجين دون غيرهم.

وفي هذا المقال سوف نأخذ برنامجاً صغيراً واحداً يحسب طلبات العملاء من ملف نصي، ونكتبه بلغتي Python وJavaScript، ونجعله يفشل بطرق حقيقية، ثم نصلحه خطوة بخطوة. وكل المخرجات التي ستراها هنا خرجت فعلاً من تشغيل البرامج داخل Docker، ولم نكتب أي سطر منها باليد.

وسوف نناقش في هذا المقال ما يلي:

- ما معنى التصحيح Debugging، ومن أين جاءت كلمة Bug، مع صورة أشهر «حشرة» في تاريخ الحاسب.
- قراءة ال Traceback في Python سطراً بسطر: آخر سطر أولاً، ثم الإطارات Frames، ثم علامات `^^^`.
- الاستثناء المتسلسل Chained Exception، ولماذا ترى أحياناً رسالتي خطأ في مخرج واحد.
- أشهر رسائل الخطأ التي يقابلها المبتدئ في Python ومعنى كل واحدة.
- ال Stack Trace في Node.js وترتيبه المعكوس، وخطأ حقيقي لم يرم أي استثناء وأعطى `NaN` بصمت.
- طريقة منظمة للتصحيح: أعد إنتاج الخطأ، ثم ضع فرضية، ثم اختبرها، بدلاً من تغيير الكود عشوائياً.
- التصحيح بالطباعة Print Debugging، والمصحح Debugger في الطرفية مع pdb، ونقاط التوقف Breakpoints في VS Code.
- البحث الثنائي Bisecting بـ `git bisect` لإيجاد ال Commit الذي أدخل الخطأ، وفي ملف بيانات من 5000 سطر.
- ماذا يحدث إذا ابتلعت الخطأ، والأخطاء الشائعة، وقائمة تحقق قبل أن تطلب المساعدة.

## ما معنى التصحيح Debugging، ومن أين جاءت كلمة Bug؟

الخلل البرمجي Bug هو أي فرق بين ما تريده من البرنامج وما يفعله فعلاً، سواءً ظهر على شكل رسالة خطأ توقف البرنامج، أو نتيجة خاطئة، أو بطء غير مفهوم. والتصحيح Debugging هو العمل الذي تقوم به لتجد سبب هذا الفرق وتزيله. ولاحظ أن الكلمة المهمة هنا هي «السبب»، فإخفاء العرض، مثل أن تضع `try` حول السطر الذي يفشل وتتجاهل الخطأ، ليس تصحيحاً، وإنما تأجيل للمشكلة إلى وقت أسوأ، كما سوف نرى في قسم ابتلاع الخطأ.

والكلمة نفسها أقدم من الحاسب، فقد كان المهندسون يسمون عيوب الأجهزة bugs قبل ظهور الحاسبات، ولكن القصة التي يتذكرها الجميع حدثت في جامعة Harvard مع حاسب Mark II، وهو حاسب كان يعمل بالمرحلات الكهربائية Relays.

![صفحة من سجل تشغيل حاسب Mark II مكتوبة بخط اليد، وفي وسطها فراشة ليلية ملصقة بشريط لاصق، وبجانبها جملة First actual case of bug being found](https://arabroot.io/content/images/2026/10/debugging-basics-07-first-bug-1947.webp)

صفحة سجل حاسب Harvard Mark II وفيها الفراشة الليلية الملصقة في 9 سبتمبر 1947\. الصورة: Naval Surface Warfare Center, Dahlgren، ملكية عامة Public Domain، المصدر: [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:First%5FComputer%5FBug,%5F1947.jpg?ref=arabroot.io) (تم تصغيرها وتحويلها إلى WebP).

🦋

في 9 سبتمبر 1947 وجد فريق تشغيل حاسب Mark II في جامعة Harvard فراشة ليلية Moth عالقة بين نقاط المرحل رقم 70 في اللوحة F، فأخرجوها وألصقوها في سجل التشغيل وكتبوا بجانبها «First actual case of bug being found»، أي أول حالة حقيقية يوجد فيها bug. وبحسب [وصف الصورة في Wikimedia Commons](https://commons.wikimedia.org/wiki/File:First%5FComputer%5FBug,%5F1947.jpg?ref=arabroot.io) فإن كلمة debugging كانت مستخدمة قبل ذلك، ولهذا كانت الجملة نكتة من الفريق، لأنهم وجدوا هذه المرة حشرة حقيقية. وتنسب القصة عادة إلى Grace Hopper التي كانت تعمل على الحاسب نفسه، والسجل محفوظ اليوم في متحف Smithsonian. والدرس الذي يهمنا منها أن الفريق لم يكتف بإعادة تشغيل الحاسب، وإنما فتح اللوحة ووجد السبب المادي وسجله في دفتر التشغيل، وهذا بالضبط ما سوف نفعله مع الكود.

والتصحيح اليوم يأخذ من وقت المبرمج أكثر مما يتوقع المبتدئ، فأنت تكتب السطر مرة وتقرؤه وتصححه مرات، ولذلك فإن تعلم أدواته في البداية يوفر عليك ساعات طويلة لاحقاً. وقد شرحنا في مقال [كيف تتعامل لغات البرمجة مع الأخطاء](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1/) كيف ترمي اللغات الاستثناءات أو ترجع الخطأ كقيمة، أما هنا فنتعامل مع الجانب العملي: الخطأ وقع، فكيف تقرؤه وتصل إلى سببه؟

## المختبر: برنامج طلبات صغير في Python وفي JavaScript

لنفرض أن لديك متجراً صغيراً، ويصدر نظام المبيعات ملفاً نصياً فيه طلب في كل سطر، اسم العميل ثم فاصلة ثم مبلغ الطلب بالريال، وتريد برنامجاً يطبع لكل عميل عدد طلباته ومتوسط مبلغها. وهذا هو الملف `orders.txt` الذي سوف نبدأ به:

```text
huda,120
omar,45
huda,80
sara,60
```

وسوف نشغل كل شيء داخل Containers من Docker Images رسمية بإصدارات مثبتة، حتى تحصل على المخرج نفسه إذا أعدت التجربة، والجدول التالي يبين ما استخدمناه:

| الأداة             | Docker Image                        | الإصدار في المختبر                  |
| ------------------ | ----------------------------------- | ----------------------------------- |
| Python وpdb        | python:3.13.17-slim                 | Python 3.13.17                      |
| Node.js            | node:22-alpine                      | Node.js v22.23.4                    |
| git bisect         | python:3.13.17-slim مع حزمة git     | git 2.47.3                          |
| VS Code في المتصفح | codercom/code-server:4.144.0-trixie | code-server 4.144.0 مع Code 1.144.0 |

وكل ملفات المختبر في مجلد واحد فيه `python` و`node` و`bisect`، ونشغل البرنامج بأمر مثل التالي، حيث يربط الخيار `-v` المجلد بالمسار `/lab` داخل ال Container للقراءة فقط، والخيار `--rm` يحذف ال Container بعد انتهاء الأمر:

```bash
docker run --rm --name scp2-dbg-py -v "$PWD/python:/lab:ro" -w /lab \
  python:3.13.17-slim python3 orders.py
```

وهذا هو البرنامج بلغة Python، وقد قسمناه إلى دوال صغيرة عن قصد، لأن هذا ما سوف يجعل ال Traceback مفيداً كما سوف نرى:

```python
# Prints how many orders each customer placed and the average amount, from a text file "customer,amount".
import sys

def parse_line(line):
    customer, amount = line.strip().split(",")
    return customer, int(amount)

def load_orders(path):
    orders = []
    with open(path, encoding="utf-8") as f:
        for line in f:
            orders.append(parse_line(line))
    return orders

def average(values):
    return sum(values) / len(values)

def report(orders):
    by_customer = {}
    for customer, amount in orders:
        by_customer.setdefault(customer, []).append(amount)
    for customer, amounts in by_customer.items():
        print(f"{customer}: {len(amounts)} orders, average {average(amounts):.2f}")

def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "orders.txt"
    report(load_orders(path))

if __name__ == "__main__":
    main()
```

ومع الملف السليم يعمل البرنامج كما نريد، والمخرج سوف يكون كما يلي:

```text
huda: 2 orders, average 100.00
omar: 1 orders, average 45.00
sara: 1 orders, average 60.00
```

هدى لها طلبان بمتوسط 100 ريال، أي 120 و80، وعمر وسارة طلب واحد لكل منهما. والآن سوف نعطي البرنامج ملفات من العالم الحقيقي، والعالم الحقيقي لا يرسل الملفات بالشكل الذي تتوقعه دائماً.

## كيف تقرأ ال Traceback في Python سطراً بسطر؟

في الملف الثاني `orders-decimal.txt` كتب النظام مبلغ طلب عمر بالهللات، أي `45.5` بدلاً من `45`، وهذا ما حدث عندما شغلنا البرنامج عليه:

```text
Traceback (most recent call last):
  File "/lab/orders.py", line 36, in <module>
    main()
    ~~~~^^
  File "/lab/orders.py", line 32, in main
    report(load_orders(path))
           ~~~~~~~~~~~^^^^^^
  File "/lab/orders.py", line 14, in load_orders
    orders.append(parse_line(line))
                  ~~~~~~~~~~^^^^^^
  File "/lab/orders.py", line 7, in parse_line
    return customer, int(amount)
                     ~~~^^^^^^^^
ValueError: invalid literal for int() with base 10: '45.5'
```

هذا المخرج اسمه في Python ال Traceback، وفي أغلب اللغات الأخرى اسمه ال Stack Trace، والفكرة واحدة: صورة لحظية لسلسلة الدوال التي كانت تعمل لحظة وقوع الخطأ، من أول دالة بدأ بها البرنامج حتى الدالة التي فشلت. والخطأ الذي يقع فيه كل مبتدئ تقريباً هو أن يقرأ هذا المخرج من الأعلى إلى الأسفل كما يقرأ أي نص، فيبدأ بـ `main()` ويضيع في التفاصيل قبل أن يعرف ما المشكلة أصلاً. والترتيب الصحيح هو العكس، كما يبين الشكل التالي:

![ال Traceback الحقيقي لبرنامج orders.py في طرفية داكنة، مع ثلاث علامات مرقمة: رقم 1 على آخر سطر ValueError باللون الأحمر، ورقم 2 على آخر إطار في parse_line سطر 7، ورقم 3 على أول سطر Traceback most recent call last، وعلى اليمين شرح كل خطوة](https://arabroot.io/content/images/2026/10/debugging-basics-01-traceback.webp)

اقرأ آخر سطر أولاً، ثم آخر إطار، ثم اصعد لترى من استدعى من

### ابدأ من آخر سطر: نوع الاستثناء Exception Type ورسالته

آخر سطر في ال Traceback هو أهم سطر، وفيه جزءان يفصل بينهما النقطتان: نوع الاستثناء Exception Type، وهو هنا `ValueError`، ثم الرسالة. ونوع الاستثناء وحده يضيق دائرة البحث كثيراً، فـ `ValueError` في Python يعني أن الدالة استلمت قيمة من النوع الصحيح ولكن محتواها غير مقبول، فالدالة `int()` تقبل النصوص، ولكن ليس أي نص. وهذا يختلف عن `TypeError` الذي يعني أن نوع القيمة نفسه خطأ، كأن تجمع نصاً مع رقم.

ثم الرسالة: `invalid literal for int() with base 10: '45.5'`، أي أن النص `'45.5'` لا يمثل عدداً صحيحاً بالنظام العشري. ولاحظ أن Python وضعت القيمة نفسها في الرسالة بين علامتي اقتباس، وهذه معلومة ذهبية، فأنت الآن تعرف أن المشكلة ليست في الكود وحده، وإنما في بيانات فيها أرقام عشرية. وبهذا السطر الواحد عرفت نوع الخطأ وسببه المباشر قبل أن تنظر إلى أي سطر آخر.

### الإطارات Frames: من استدعى من؟

كل مجموعة من سطرين أو ثلاثة تبدأ بكلمة `File` اسمها إطار Frame، وهي تمثل دالة واحدة كانت تعمل لحظة الخطأ، وفيها اسم الملف ورقم السطر واسم الدالة، ثم نص السطر نفسه. وهذه الإطارات هي نفس إطارات ال Stack التي شرحناها في قسم [ال Stack في مقال الذاكرة للمبرمجين](https://arabroot.io/articles/%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86-stack-%D9%88-heap-%D9%88%D8%A7%D9%84%D9%85%D8%A4%D8%B4%D8%B1%D8%A7%D8%AA/#stack)، فكل استدعاء لدالة يضيف إطاراً فوق ال Stack، وال Traceback يطبع هذه الإطارات كما كانت لحظة الخطأ.

والجملة في أول سطر `most recent call last` تخبرك بالترتيب صراحة: الاستدعاء الأحدث في الأسفل. فاقرأ الإطارات من الأسفل إلى الأعلى كما يلي:

- `line 7, in parse_line`: آخر إطار، وهو المكان الذي وقع فيه الخطأ فعلاً، أي السطر `return customer, int(amount)`.
- `line 14, in load_orders`: الدالة التي استدعت `parse_line`، وهي التي تقرأ الملف سطراً سطراً.
- `line 32, in main`: الدالة التي استدعت `load_orders`.
- `line 36, in <module>`: المستوى الأعلى من الملف، أي الكود الذي ليس داخل أي دالة، وهو السطر `main()`.

وقد يتساءل البعض: إذا كان الخطأ في السطر 7، فلماذا أحتاج إلى بقية الإطارات؟ والإجابة أن مكان وقوع الخطأ ليس دائماً مكان إصلاحه، فالسطر 7 يفعل ما طلبناه منه بالضبط، وهو تحويل النص إلى `int`، والسؤال الحقيقي هو: هل يجب أن تقبل الدالة مبالغ بالهللات؟ أم يجب أن يرفض `load_orders` الملف برسالة واضحة؟ أم أن الخطأ في النظام الذي أصدر الملف؟ والإطارات تريك الطريق الذي جاءت منه القيمة الخاطئة، فتقرر أين تصلح.

وفي المشاريع الحقيقية سوف تجد بين إطاراتك إطارات من مكتبات خارجية أو من داخل اللغة نفسها، مثل ملفات في مجلد `site-packages`، والقاعدة أن تبحث عن آخر إطار من ملفاتك أنت، لأن الاحتمال الأكبر أن الخطأ في طريقة استخدامك للمكتبة وليس في المكتبة نفسها.

### علامات ^^^ والمواقع الدقيقة للخطأ Fine-grained Error Locations

تحت كل سطر كود في ال Traceback ترى علامات مثل `~~~^^^^^^^^`، وهذه أضيفت في Python 3.11 بحسب [المقترح PEP 657](https://peps.python.org/pep-0657/?ref=arabroot.io)، وهدفها أن تحدد لك التعبير الذي فشل داخل السطر وليس السطر كله. ففي إطار `parse_line` تقع العلامات تحت `int(amount)`، حيث تشير `~~~` إلى اسم الدالة `int` وتشير `^^^^^^^^` إلى الاستدعاء نفسه، وفي إطار `main` تقع تحت `load_orders(path)` وليس تحت `report`، فتعرف أن الخطأ خرج من القراءة وليس من الطباعة. وهذه المعلومة مفيدة جداً عندما يكون في السطر الواحد عدة استدعاءات، مثل `a.b.c.d`، فقبل هذه الإضافة كنت ترى السطر وتخمن أي جزء منه هو المشكلة.

وفي Python 3.13 أصبحت ال Tracebacks ملونة في الطرفية كما في [صفحة ما الجديد في Python 3.13](https://docs.python.org/3/whatsnew/3.13.html?ref=arabroot.io)، ويمكن إيقاف الألوان بالمتغيرين `PYTHON_COLORS` و`NO_COLOR`. ولن ترى الألوان هنا لأن المخرج مر عبر Docker دون طرفية تفاعلية.

### نفس الخطأ ValueError بسبب مختلف: سطر فارغ في نهاية الملف

الملف الثالث `orders-blank.txt` سليم تماماً، ولكن في آخره سطر فارغ، وهذا يحدث كثيراً عندما يحفظ أحدهم الملف من محرر نصوص ويضغط Enter بعد آخر سطر. وهذا ما حدث:

```text
Traceback (most recent call last):
  File "/lab/orders.py", line 36, in <module>
    main()
    ~~~~^^
  File "/lab/orders.py", line 32, in main
    report(load_orders(path))
           ~~~~~~~~~~~^^^^^^
  File "/lab/orders.py", line 14, in load_orders
    orders.append(parse_line(line))
                  ~~~~~~~~~~^^^^^^
  File "/lab/orders.py", line 6, in parse_line
    customer, amount = line.strip().split(",")
    ^^^^^^^^^^^^^^^^
ValueError: not enough values to unpack (expected 2, got 1)
```

في المخرج أعلاه لاحظ التالي:

- نوع الاستثناء هو نفسه `ValueError`، ولكن الرسالة مختلفة تماماً: `not enough values to unpack (expected 2, got 1)`، أي أن الكود طلب فك قيمتين من القائمة ولم يجد إلا واحدة. ولهذا لا يكفي أن تبحث عن نوع الخطأ وحده، فالرسالة هي التي تحدد السبب.
- آخر إطار الآن هو السطر 6 وليس السطر 7، والعلامات `^^^^^^^^^^^^^^^^` تقع تحت `customer, amount` أي تحت عملية الفك Unpacking نفسها.
- وإذا فكرت في السطر الفارغ فسوف تفهم الرقم 1: النص الفارغ `""` بعد `strip()` يعطي مع `split(",")` قائمة فيها عنصر واحد هو `['']`، وليس قائمة فارغة.

ولكن هناك مشكلة أكبر في المخرجين السابقين، وهي أن ال Traceback يقول لك أين فشل الكود، ولا يقول في أي سطر من ملف البيانات. وعندما يكون الملف 4 أسطر فهذا لا يهم، ولكن عندما يكون 50 ألف سطر فسوف تحتاج إلى رقم السطر. وهذا ما سوف نصلحه في القسم التالي.

## الاستثناء المتسلسل Chained Exception: لماذا ترى Traceback مرتين؟

الحل هو أن تلتقط الخطأ في المكان الذي يعرف السياق، وهو `load_orders` لأنها تعرف اسم الملف ورقم السطر، ثم ترمي خطأ جديداً برسالة أوضح، مع الاحتفاظ بالخطأ الأصلي كسبب. وفي Python يتم ذلك بالصيغة `raise ... from e` كما في [توثيق جملة raise](https://docs.python.org/3/reference/simple%5Fstmts.html?ref=arabroot.io#the-raise-statement). وهذه هي الدالة بعد التعديل، وفيها أيضاً تجاوز الأسطر الفارغة:

```python
def load_orders(path):
    orders = []
    with open(path, encoding="utf-8") as f:
        for number, line in enumerate(f, start=1):
            if not line.strip():
                continue  # skip blank lines, such as the empty line at the end of a file
            try:
                orders.append(parse_line(line))
            except ValueError as e:
                raise ValueError(f"{path}, line {number}: {line.strip()!r}") from e
    return orders
```

ومع الملف الذي فيه سطر فارغ يعمل البرنامج الآن ويطبع النتيجة الصحيحة نفسها التي رأيناها مع الملف السليم. أما مع ملف الهللات فالمخرج سوف يكون كما يلي:

```text
Traceback (most recent call last):
  File "/lab/orders_v2.py", line 17, in load_orders
    orders.append(parse_line(line))
                  ~~~~~~~~~~^^^^^^
  File "/lab/orders_v2.py", line 7, in parse_line
    return customer, int(amount)
                     ~~~^^^^^^^^
ValueError: invalid literal for int() with base 10: '45.5'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/lab/orders_v2.py", line 41, in <module>
    main()
    ~~~~^^
  File "/lab/orders_v2.py", line 37, in main
    report(load_orders(path))
           ~~~~~~~~~~~^^^^^^
  File "/lab/orders_v2.py", line 19, in load_orders
    raise ValueError(f"{path}, line {number}: {line.strip()!r}") from e
ValueError: orders-decimal.txt, line 2: 'omar,45.5'
```

هذا مخرج فيه Traceback مرتين، وهو ما يربك المبتدئ عادة، فأيهما الخطأ الحقيقي؟ والجملة التي بينهما هي المفتاح: `The above exception was the direct cause of the following exception`، أي أن الاستثناء الأعلى هو السبب المباشر للاستثناء الأسفل. ولذلك اقرأه هكذا:

- ابدأ من آخر سطر في المخرج كله: `orders-decimal.txt, line 2: 'omar,45.5'`، وهذه هي الرسالة التي كتبناها، وفيها الآن اسم الملف ورقم السطر والسطر نفسه، فتفتح الملف وتذهب إلى السطر 2 مباشرة.
- ثم اقرأ الجزء الأعلى إذا احتجت إلى السبب التقني، وهو نفس `ValueError` من `int()` الذي رأيناه في البداية. ولاحظ أن الجزء الأعلى يبدأ من `load_orders` وليس من `<module>`، لأنه يعرض فقط الإطارات بين مكان الالتقاط ومكان الخطأ.

وهناك جملة أخرى قد تراها بين جزأين: `During handling of the above exception, another exception occurred`، وهذه تعني أن خطأ ثانياً وقع أثناء معالجة الأول دون أن تقصد ربطهما، مثل خطأ داخل كتلة `except` نفسها، وهي عادة علامة على خطأ في كود المعالجة. وكلا الحالتين موثق في [درس الأخطاء والاستثناءات في توثيق Python](https://docs.python.org/3/tutorial/errors.html?ref=arabroot.io). والقاعدة العملية: عندما تلتقط خطأ وترمي غيره، استخدم `from e` دائماً، والسبب أن من يقرأ السجل بعدك يحتاج إلى السبب الأصلي، وبدونه يرى رسالتك وحدها ويضيع الطريق إلى السطر الذي فشل فعلاً.

## أشهر رسائل الخطأ Error Messages للمبتدئين في Python

بعد فترة قصيرة من البرمجة سوف تلاحظ أن معظم الأخطاء التي تقابلها تتكرر من مجموعة صغيرة من الأنواع، ولذلك كتبنا سكربتاً يشغل ثمانية أسطر خاطئة نموذجية ويطبع آخر سطر من ال Traceback لكل واحد منها، والمخرج سوف يكون كما يلي:

```text
NameError: name 'total' is not defined
TypeError: can only concatenate str (not "int") to str
KeyError: 'coffee'
IndexError: list index out of range
AttributeError: 'NoneType' object has no attribute 'group'
ZeroDivisionError: division by zero
ModuleNotFoundError: No module named 'requests'
FileNotFoundError: [Errno 2] No such file or directory: 'ordrs.txt'
```

والجدول التالي يشرح كل رسالة، وأين تبحث عن سببها عادة:

| الخطأ                       | السطر الذي سببه       | المعنى وأين تبحث                                                                                       |
| --------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------ |
| NameError                   | print(total)          | اسم متغير أو دالة غير معروف: خطأ إملائي، أو متغير عرفته داخل دالة أخرى، أو نسيت أن تعرفه.              |
| TypeError                   | 'orders: ' + 3        | نوع القيمة لا يناسب العملية: نص مع رقم، أو استدعاء دالة بعدد معاملات خاطئ.                             |
| KeyError                    | prices\['coffee'\]    | مفتاح غير موجود في القاموس dict، والرسالة هي المفتاح نفسه. استخدم get() إذا كان غيابه متوقعاً.         |
| IndexError                  | orders\[3\]           | رقم عنصر خارج حدود القائمة، وقائمة من 3 عناصر آخر رقم فيها 2 لأن الترقيم يبدأ من 0.                    |
| AttributeError على NoneType | m.group()             | متغير قيمته None وأنت تعامله كأنه كائن، وغالباً دالة رجعت None لأنها لم تجد شيئاً، مثل re.match هنا.   |
| ZeroDivisionError           | sum(\[\]) / len(\[\]) | قسمة على صفر، وغالباً متوسط لقائمة فارغة.                                                              |
| ModuleNotFoundError         | import requests       | المكتبة غير مثبتة في البيئة التي تشغل منها البرنامج، أو أنك في بيئة افتراضية Virtual Environment أخرى. |
| FileNotFoundError           | open('ordrs.txt')     | المسار خاطئ، أو أن مجلد العمل Working Directory ليس المجلد الذي تظنه.                                  |

ولاحظ أن رسالة `NameError` هنا لم تقترح شيئاً، والسبب أننا شغلنا الأسطر بالدالة `exec` داخل السكربت. أما عندما تشغل الخطأ نفسه في ملف عادي فإن Python تساعدك أكثر، وهذا ملف صغير كتبنا فيه اسم المتغير `totl` ثم طبعنا `total`:

```text
Traceback (most recent call last):
  File "/lab/typo.py", line 5, in <module>
    print(f"total: {total}")
                    ^^^^^
NameError: name 'total' is not defined. Did you mean: 'totl'?
```

الجملة `Did you mean: 'totl'?` أضيفت في Python 3.10 لأخطاء `NameError` و`AttributeError` كما في [صفحة ما الجديد في Python 3.10](https://docs.python.org/3/whatsnew/3.10.html?ref=arabroot.io)، وهي تقارن الاسم الخاطئ بالأسماء الموجودة وتقترح أقربها. والدرس هنا أن الرسائل الحديثة تقول لك أحياناً الحل نفسه، فاقرأها حتى آخر كلمة قبل أن تبحث عنها في أي مكان.

## ال Stack Trace في Node.js: الترتيب معكوس

الآن نكتب البرنامج نفسه بلغة JavaScript ونشغله بـ Node.js. والكود قريب جداً من نسخة Python، فهو يقرأ الملف كله ويقسمه على علامة نهاية السطر، ثم يفك كل سطر إلى اسم ومبلغ:

```javascript
// Prints how many orders each customer placed and the average amount, from a text file "customer,amount".
const fs = require("node:fs");

function parseLine(line) {
  const [customer, amount] = line.trim().split(",");
  return [customer, Number(amount)];
}

function loadOrders(path) {
  const text = fs.readFileSync(path, "utf8");
  return text.split("\n").map(parseLine);
}

function average(values) {
  return values.reduce((a, b) => a + b, 0) / values.length;
}

function report(orders) {
  const byCustomer = new Map();
  for (const [customer, amount] of orders) {
    if (!byCustomer.has(customer)) byCustomer.set(customer, []);
    byCustomer.get(customer).push(amount);
  }
  for (const [customer, amounts] of byCustomer) {
    console.log(`${customer}: ${amounts.length} orders, average ${average(amounts).toFixed(2)}`);
  }
}

report(loadOrders(process.argv[2] || "orders.txt"));
```

### المشكلة التي لم ترم أي خطأ: NaN بصمت

شغلنا البرنامج على الملف السليم نفسه الذي نجح مع Python، فكان المخرج كما يلي:

```text
huda: 2 orders, average 100.00
omar: 1 orders, average 45.00
sara: 1 orders, average 60.00
: 1 orders, average NaN
exit=0
```

ظهر عميل رابع ليس له اسم، وله طلب واحد بمتوسط `NaN`، والبرنامج انتهى برمز الخروج 0 أي «نجاح». وهذه المشكلة لم نخطط لها، وإنما ظهرت عند أول تشغيل، والسبب أن الملف السليم نفسه ينتهي بعلامة نهاية سطر بعد `sara,60` كما تنتهي أغلب الملفات النصية، والدالة `split("\n")` تعطي بعد آخر علامة نصاً فارغاً. وفي Python لم نر هذه المشكلة مع الملف السليم، لأن القراءة بـ `for line in f` لا تعطي سطراً بعد آخر علامة نهاية سطر.

ولكن لماذا لم يفشل Node.js كما فشلت Python مع السطر الفارغ؟ والإجابة في سطرين من الكود: فك القائمة `const [customer, amount] = ['']` لا يرمي خطأ في JavaScript، وإنما يعطي `amount` القيمة `undefined`، ثم `Number(undefined)` لا يرمي خطأ أيضاً، وإنما يرجع `NaN`، وهي قيمة خاصة معناها «ليس رقماً» Not a Number كما في [توثيق NaN في MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/NaN?ref=arabroot.io)، وأي عملية حسابية عليها ترجع `NaN` أيضاً. وكذلك مع ملف الهللات، فقد قبل `Number("45.5")` القيمة وأعطى عمر متوسط `45.50` دون أي اعتراض، بينما رفضتها Python لأننا طلبنا `int`.

وهذا النوع من الأخطاء أخطر من الرسالة الحمراء بكثير، لأن البرنامج يكمل ويكتب نتيجة خاطئة في تقرير أو قاعدة بيانات، وقد لا يلاحظها أحد إلا بعد أسابيع. ولذلك فإن أفضل خطأ هو الذي يظهر مبكراً وبصوت عال، وهذه القاعدة تسمى Fail Fast: تحقق من البيانات عند دخولها، وأوقف البرنامج برسالة واضحة إذا كانت خاطئة، بدلاً من أن تتركها تسير حتى تفسد النتيجة.

### قراءة ال Stack Trace في Node.js سطراً بسطر

كثير من المطورين يكتبون `parseLine` بطريقة أخرى، فيقص كل جزء على حدة بـ `trim()`، وهذا ما حدث مع الملف الذي فيه سطر فارغ عندما كتبنا السطر كما يلي:

```javascript
function parseLine(line) {
  const [customer, amount] = line.split(",");
  return [customer.trim(), Number(amount.trim())];
}
```

والمخرج سوف يكون كما يلي:

```text
/lab/orders_trim.js:6
  return [customer.trim(), Number(amount.trim())];
                                         ^

TypeError: Cannot read properties of undefined (reading 'trim')
    at parseLine (/lab/orders_trim.js:6:42)
    at Array.map (<anonymous>)
    at loadOrders (/lab/orders_trim.js:11:27)
    at Object.<anonymous> (/lab/orders_trim.js:20:8)
    at Module._compile (node:internal/modules/cjs/loader:1781:14)
    at Object..js (node:internal/modules/cjs/loader:1913:10)
    at Module.load (node:internal/modules/cjs/loader:1505:32)
    at Function._load (node:internal/modules/cjs/loader:1309:12)
    at wrapModuleLoad (node:internal/modules/cjs/loader:254:19)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)

Node.js v22.23.4
```

هنا ترتيب القراءة مختلف عن Python، كما يبين الشكل التالي:

![عمودان: في عمود Python الأزرق الإطارات من module إلى main إلى load_orders إلى parse_line وتحتها رسالة ValueError وسهم يتجه للأسفل، وفي عمود Node.js البرتقالي رسالة TypeError في الأعلى ثم at parseLine ثم Array.map ثم loadOrders ثم إطار Module._compile الداخلي، وسهم يتجه للأعلى](https://arabroot.io/content/images/2026/10/debugging-basics-02-two-orders.webp)

في Python مكان الخطأ في الأسفل، وفي Node.js الرسالة ومكان الخطأ في الأعلى

في المخرج أعلاه لاحظ التالي:

- أول ثلاثة أسطر يطبعها Node.js قبل ال Stack Trace: المسار ورقم السطر `/lab/orders_trim.js:6`، ثم السطر نفسه، ثم علامة `^` تحت المكان الذي فشل، وهو هنا `.trim()` الثانية على `amount` وليس الأولى على `customer`.
- السطر `TypeError: Cannot read properties of undefined (reading 'trim')` هو نوع الخطأ ورسالته، وجاء في البداية وليس في النهاية. ومعناه أن الكود حاول قراءة الخاصية `trim` من قيمة هي `undefined`، أي أن `amount` لم يأخذ أي قيمة، وهذه أشهر رسالة في JavaScript على الإطلاق.
- أول سطر يبدأ بـ `at` هو الإطار الأحدث، أي مكان الخطأ: `parseLine` في الملف `orders_trim.js` السطر 6 العمود 42\. فالترتيب هنا عكس Python، الأحدث في الأعلى.
- `at Array.map (<anonymous>)` إطار من داخل JavaScript نفسها، فالدالة `map` هي التي استدعت `parseLine` لكل سطر، ولا يوجد لها ملف لأنها مكتوبة داخل المحرك.
- الأسطر التي تبدأ بـ `node:internal` هي كود Node.js الذي يحمل ملفك ويشغله، وتجاهلها في الغالب، فلن تجد خطأك فيها.
- عدد أسطر `at` هنا عشرة بالضبط، وهذا ليس صدفة، فمحرك V8 يجمع افتراضياً عشرة إطارات فقط، ويمكنك تغيير هذا العدد بالمتغير `Error.stackTraceLimit` كما في [توثيق Stack Trace API في V8](https://v8.dev/docs/stack-trace-api?ref=arabroot.io). وإذا كان برنامجك عميقاً فقد تختفي أول الإطارات من القائمة، وهذا يربك من يبحث عن بداية الطريق.
- السطر الأخير `Node.js v22.23.4` هو إصدار Node.js، وأضفه دائماً عندما تطلب المساعدة أو تكتب تقرير خطأ.

### الدوال غير المتزامنة async و await في ال Stack Trace

في البرامج الحقيقية تقرأ الملفات في Node.js غالباً بالدوال غير المتزامنة، حتى لا يتوقف البرنامج كله أثناء القراءة. ولذلك كتبنا النسخة نفسها بـ `fs/promises` و`await`:

```javascript
async function loadOrders(path) {
  const text = await fs.readFile(path, "utf8");
  return text.split("\n").map(parseLine);
}

async function main() {
  const orders = await loadOrders(process.argv[2] || "orders.txt");
  console.log(`${orders.length} orders`);
}

main();
```

والمخرج مع الملف نفسه سوف يكون كما يلي:

```text
TypeError: Cannot read properties of undefined (reading 'trim')
    at parseLine (/lab/orders_async.js:6:42)
    at Array.map (<anonymous>)
    at loadOrders (/lab/orders_async.js:11:27)
    at async main (/lab/orders_async.js:15:18)
```

لاحظ الكلمة `async` في آخر إطار: `at async main`. فعندما تنتظر الدالة `main` نتيجة `loadOrders` بـ `await`، فإنها لا تكون على ال Stack فعلاً أثناء قراءة الملف، لأن القراءة تتم في الخلفية والبرنامج يرجع إلى حلقة الأحداث Event Loop، ثم يكمل بعد انتهائها. ولكن V8 يحتفظ بسلسلة ال `await` ويعرضها لك بهذه الكلمة حتى تعرف من كان ينتظر من. ولاحظ أيضاً أن إطارات `node:internal` اختفت هنا، لأن الكود بعد `await` يكمل من حلقة الأحداث وليس من محمل الملفات.

### إصلاح نسخة JavaScript بقاعدة Fail Fast: تجاوز الأسطر الفارغة ورفض المبالغ الخاطئة

والإصلاح يطبق قاعدة Fail Fast التي ذكرناها: نتجاوز الأسطر الفارغة، ونرفض أي سطر ليس فيه اسم أو مبلغه ليس رقماً، برسالة فيها الملف ورقم السطر، كما فعلنا مع Python:

```javascript
function parseLine(line, number, path) {
  const [customer, amount] = line.trim().split(",");
  const value = Number(amount);
  if (!customer || Number.isNaN(value)) {
    throw new Error(`${path}, line ${number}: ${JSON.stringify(line)}`);
  }
  return [customer, value];
}

function loadOrders(path) {
  const text = fs.readFileSync(path, "utf8");
  return text
    .split("\n")
    .map((line, i) => [line, i + 1])
    .filter(([line]) => line.trim() !== "")
    .map(([line, number]) => parseLine(line, number, path));
}
```

مع الملف الذي فيه سطر فارغ اختفى العميل الذي بلا اسم، وطبع البرنامج ثلاثة عملاء فقط كما في Python. ثم جربناه على ملف كتب فيه أحدهم الحرف O بدلاً من الصفر في `4O`، وهو خطأ حقيقي يحدث عند الإدخال اليدوي، فكان المخرج كما يلي:

```text
/lab/orders_fixed.js:8
    throw new Error(`${path}, line ${number}: ${JSON.stringify(line)}`);
    ^

Error: orders-letter.txt, line 2: "omar,4O"
    at parseLine (/lab/orders_fixed.js:8:11)
    at /lab/orders_fixed.js:19:30
    at Array.map (<anonymous>)
    at loadOrders (/lab/orders_fixed.js:19:6)
    at Object.<anonymous> (/lab/orders_fixed.js:37:8)
```

والآن الرسالة تقول بالضبط ما تحتاجه: الملف، والسطر 2، والنص كما هو، ولاحظ الإطار `at /lab/orders_fixed.js:19:30` الذي ليس له اسم، فهو الدالة السهمية Arrow Function التي مررناها إلى `map`، والدوال التي بلا اسم تظهر في ال Stack Trace بالموقع فقط، وهذا سبب جيد لتسمية الدوال المهمة.

## طريقة التصحيح Debugging: لا تغير الكود قبل أن تعرف السبب

حتى الآن كانت رسائل الخطأ تدلنا على المكان، ولكن قبل أن ننتقل إلى الأدوات لنتفق على الطريقة، لأن الأداة دون طريقة تتحول إلى تجربة عشوائية. والطريقة التي يستخدمها المبرمجون المحترفون، بأسماء مختلفة، تتكون من ست خطوات كما في الشكل التالي:

![حلقة من ست خطوات مرقمة: 1 أعد إنتاج الخطأ Reproduce مع أيقونة طرفية، 2 اقرأ رسالة الخطأ مع أيقونة ملف أحمر، 3 ضع فرضية مع أيقونة شخص، 4 اختبرها بالطباعة أو المصحح أو bisect مع أيقونة حشرة، 5 أصلح السبب مع أيقونة محرر، 6 اكتب اختباراً يمنع عودته مع علامة صح، وسهم أحمر متقطع يرجع من الخطوة 4 إلى 3، وفي الوسط جملة لا تغير الكود قبل أن تعرف السبب](https://arabroot.io/content/images/2026/10/debugging-basics-03-method.webp)

الحلقة تتكرر بين الفرضية والاختبار حتى تثبت إحداها، ثم تصلح وتكتب اختباراً

1. **أعد إنتاج الخطأ Reproduce**: قبل أي شيء، اجعل الخطأ يحدث أمامك عند الطلب، بأمر واحد تشغله فتراه. فإذا لم تستطع إعادة إنتاجه فلن تعرف أبداً هل أصلحته أم اختفى صدفة. وفي حالتنا كان الأمر هو تشغيل البرنامج على `orders-decimal.txt`.
2. **اقرأ رسالة الخطأ Read the trace**: بالترتيب الذي شرحناه، آخر سطر ثم آخر إطار من كودك ثم من استدعاه. وإذا لم توجد رسالة، كما في نتيجة خاطئة، فاكتب بدقة ما المتوقع وما الذي حدث: «المتوقع 200 والنتيجة 80».
3. **ضع فرضية Hypothesis**: جملة واحدة يمكن أن تكون خاطئة، مثل «الحلقة تتجاوز أول طلب». والفرضية الجيدة تقترح تجربة تثبتها أو تنفيها.
4. **اختبر الفرضية**: بالطباعة أو بالمصحح أو بالبحث الثنائي كما سوف نرى. وإذا لم تثبت، فارجع إلى الخطوة 3 بفرضية أخرى، وهذه هي الحلقة الحمراء في الشكل.
5. **أصلح السبب Fix the cause**: وليس العرض، فإذا كان السبب أن الحلقة تبدأ من 1، فالإصلاح هو بدايتها من 0، وليس إضافة 120 إلى النتيجة.
6. **اكتب اختباراً يمنع عودته Regression Test**: اختبار صغير يفشل مع الكود القديم وينجح مع الجديد، فإذا أعاد أحدهم الخطأ نفسه بعد سنة، أوقفه الاختبار قبل أن يصل إلى المستخدمين. وقد شرحنا أنواع الاختبارات في مقال [أنواع الاختبارات للمبتدئين](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/).

وأهم ما في الخطوة الأولى أن تصغر المشكلة، فإذا كان الخطأ يظهر في برنامج من ألف سطر مع ملف بيانات من مليون سطر، فحاول أن تصل إلى أصغر كود وأصغر بيانات تظهر الخطأ نفسه، وهذا ما يسمى المثال الأدنى القابل لإعادة الإنتاج Minimal Reproducible Example، وتطلبه منك مواقع مثل Stack Overflow في [صفحة إرشاداتها](https://stackoverflow.com/help/minimal-reproducible-example?ref=arabroot.io) قبل أن تسأل. والمفاجأة التي يكتشفها كل مبرمج أن نصف المشاكل تقريباً تحل نفسها أثناء التصغير، لأنك وأنت تحذف الأجزاء التي لا علاقة لها بالخطأ تقترب من الجزء الذي له علاقة.

💡

هناك أسلوب قديم ومعروف بين المبرمجين اسمه بطة المطاط Rubber Duck Debugging: تضع بطة مطاطية أو أي شيء على المكتب وتشرح لها الكود سطراً سطراً بصوت مسموع. ويبدو الأمر مضحكاً، ولكنه يعمل لأن الشرح يجبرك على قول ما يفعله كل سطر فعلاً، وليس ما تظن أنه يفعله، وعند السطر الخاطئ تسمع نفسك تقول شيئاً غير منطقي. وزميلك في الفريق يقوم بدور البطة أيضاً، بشرط ألا يقاطعك.

## التصحيح بالطباعة Print Debugging

الآن نأتي إلى النوع الأصعب: البرنامج الذي لا يطبع أي خطأ. ولنفرض أن لدينا دالة تحسب مجموع ما دفعه عميل واحد، والمتوقع لهدى هو 120 و80، أي 200:

```python
# Total amount paid by one customer. The expected total for huda is 120 + 80 = 200.
ORDERS = [("huda", 120), ("omar", 45), ("huda", 80), ("sara", 60)]

def total_for(orders, customer):
    total = 0
    for i in range(1, len(orders)):
        name, amount = orders[i]
        if name == customer:
            total += amount
    return total

if __name__ == "__main__":
    print("huda:", total_for(ORDERS, "huda"))
```

والمخرج:

```text
huda: 80
```

لا يوجد Traceback، ولا رسالة، فقط رقم خاطئ. وربما ترى الخطأ في الكود من النظرة الأولى، ولكن في برنامج حقيقي سوف يكون السطر الخاطئ مدفوناً بين مئات الأسطر، ولذلك نتعامل معه كأننا لا نراه. وأبسط أداة لهذه الحالة وأقدمها هي الطباعة: تضيف سطر `print` داخل الحلقة يطبع القيم التي تهمك في كل دورة:

```python
    for i in range(1, len(orders)):
        name, amount = orders[i]
        print(f"{i=} {name=} {amount=} {total=}")
        if name == customer:
            total += amount
```

ولاحظ الصيغة `{i=}` داخل ال f-string، فعلامة `=` بعد اسم المتغير تطبع الاسم والقيمة معاً، فلا تحتاج إلى كتابة `"i:", i` لكل متغير. والمخرج سوف يكون كما يلي:

```text
i=1 name='omar' amount=45 total=0
i=2 name='huda' amount=80 total=0
i=3 name='sara' amount=60 total=80
huda: 80
```

أول سطر يقول كل شيء: الحلقة بدأت بـ `i=1` وأول طلب رأته هو طلب عمر، أي أن طلب هدى الأول `("huda", 120)` في الموقع 0 لم يمر على الحلقة أبداً. والفرضية «الحلقة تتجاوز أول طلب» ثبتت بتشغيل واحد، والإصلاح هو `range(len(orders))`، أو الأفضل في Python أن تكتب `for name, amount in orders` وتترك الأرقام كلها.

والطباعة لها مزايا حقيقية، فهي تعمل في أي لغة وأي بيئة، على جهازك وعلى السيرفر وداخل ال Container، ولا تحتاج إلى أي إعداد، وتريك تغير القيم عبر الزمن في سطر لكل دورة، وهذا أحياناً أوضح من التوقف عند لحظة واحدة. ولكن لها عيوب يجب أن تعرفها:

- ترى فقط ما قررت طباعته مسبقاً، فإذا احتجت إلى قيمة أخرى فعليك أن تعدل الكود وتشغله من جديد، وفي برنامج يحتاج دقائق ليصل إلى الخطأ يصبح هذا مكلفاً.
- تنسى سطور الطباعة في الكود، فتصل إلى الإنتاج وتملأ السجلات بكلام لا يفهمه أحد. ولذلك ابحث عن `print(` و`console.log(` قبل كل Commit.
- ترتيب المخرج قد يخدعك، وهذا ما سوف تراه في قسم ابتلاع الخطأ، حيث ظهر سطر طبعناه بعد الخطأ قبل رسالة الخطأ نفسها.

وإذا وجدت نفسك تحتاج الطباعة نفسها مرة بعد مرة، مثل طباعة كل طلب يصل إلى خدمة، فهذه ليست طباعة تصحيح، وإنما سجل Log، ومكانها مكتبة التسجيل بمستوياتها DEBUG وINFO وERROR التي شرحناها في مقال [السجلات Logging للمطورين](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/)، لأنك تستطيع إطفاءها وإشعالها دون تعديل الكود.

## المصحح Debugger في الطرفية Terminal: أداة pdb في Python

المصحح Debugger أداة توقف البرنامج أثناء تشغيله عند سطر تحدده، ثم تتركك تنظر إلى كل المتغيرات وسلسلة الاستدعاء، وتنفذ البرنامج سطراً بسطر، وتسأل عن أي تعبير، دون أن تعدل الكود. وفي Python يأتي مع اللغة مصحح في الطرفية اسمه pdb، وأسهل طريقة لاستخدامه هي أن تضع السطر `breakpoint()` في المكان الذي تريد التوقف فيه، كما في [توثيق الدالة breakpoint](https://docs.python.org/3/library/functions.html?ref=arabroot.io#breakpoint):

```python
def total_for(orders, customer):
    total = 0
    breakpoint()
    for i in range(1, len(orders)):
        name, amount = orders[i]
        if name == customer:
            total += amount
    return total
```

وعندما تشغل البرنامج سوف يتوقف ويعرض لك الموجه `(Pdb)` لتكتب الأوامر. وحتى نحصل على مخرج حقيقي يمكن إعادته، كتبنا الأوامر في ملف ومررناه إلى البرنامج كمدخل، وهي نفس الأوامر التي كنت ستكتبها بيدك:

```text
ll
p orders[0]
n
n
p i, orders[i]
where
b 11, name == "huda"
c
p i, name, amount, total
c
```

والمخرج سوف يكون كما يلي:

```text
> /lab/total_pdb.py(7)total_for()
-> breakpoint()
(Pdb)   5  	def total_for(orders, customer):
  6  	    total = 0
  7  ->	    breakpoint()
  8  	    for i in range(1, len(orders)):
  9  	        name, amount = orders[i]
 10  	        if name == customer:
 11  	            total += amount
 12  	    return total
(Pdb) ('huda', 120)
(Pdb) > /lab/total_pdb.py(8)total_for()
-> for i in range(1, len(orders)):
(Pdb) > /lab/total_pdb.py(9)total_for()
-> name, amount = orders[i]
(Pdb) (1, ('omar', 45))
(Pdb)   /lab/total_pdb.py(16)<module>()
-> print("huda:", total_for(ORDERS, "huda"))
> /lab/total_pdb.py(9)total_for()
-> name, amount = orders[i]
(Pdb) Breakpoint 1 at /lab/total_pdb.py:11
(Pdb) > /lab/total_pdb.py(11)total_for()
-> total += amount
(Pdb) (2, 'huda', 80, 0)
(Pdb) huda: 80
```

ولنمش على هذا المخرج أمراً بأمر:

- أول سطرين يقولان أين توقف البرنامج: الملف `total_pdb.py` السطر 7 داخل الدالة `total_for`، والسهم `->` يشير إلى السطر الذي سوف ينفذ بعد ذلك. ولاحظ أن pdb توقف عند سطر `breakpoint()` نفسه، وهذا سلوك Python 3.13، فقد أصبحت `breakpoint()` تدخل المصحح فوراً كما في صفحة ما الجديد في 3.13، وكانت في الإصدارات الأقدم تتوقف عند السطر التالي.
- `ll` (اختصار longlist) يعرض الدالة كلها مع أرقام الأسطر، والسهم عند السطر 7، وهذا يغنيك عن فتح الملف في محرر آخر.
- `p orders[0]` يطبع قيمة أي تعبير، والنتيجة `('huda', 120)`، أي أن أول طلب في القائمة هو طلب هدى بـ 120، وهذا الطلب هو الذي سوف نتابع مصيره.
- `n` (اختصار next) ينفذ السطر الحالي وينتقل إلى التالي، فنفذناه مرتين ووصلنا إلى السطر 9 داخل الحلقة.
- `p i, orders[i]` أعطى `(1, ('omar', 45))`، أي أن أول دورة في الحلقة بدأت والمتغير `i` يساوي 1 وتعمل على طلب عمر، وهنا ثبتت الفرضية: الحلقة لم تمر على الموقع 0.
- `where` يطبع ال Stack الحالي، تماماً مثل ال Traceback ولكن والبرنامج ما زال يعمل: الإطار `<module>` في السطر 16 استدعى `total_for` المتوقفة في السطر 9.
- `b 11, name == "huda"` يضع نقطة توقف شرطية Conditional Breakpoint في السطر 11، فلا يتوقف البرنامج عندها إلا إذا كان الشرط صحيحاً، وهذا مفيد جداً في حلقة من آلاف الدورات تهمك منها حالة واحدة.
- `c` (اختصار continue) يكمل التشغيل حتى أقرب نقطة توقف، فتوقف في السطر 11 عند أول طلب لهدى مر على الحلقة.
- `p i, name, amount, total` أعطى `(2, 'huda', 80, 0)`: أول مرة تصل فيها هدى إلى الجمع كانت في الدورة 2 بالطلب 80، والمجموع ما زال 0، أي أن الطلب 120 لم يحسب أبداً.
- آخر `c` أكمل البرنامج حتى نهايته وطبع `huda: 80`.

وهناك أوامر أخرى ستحتاجها كثيراً، كلها في [توثيق pdb](https://docs.python.org/3/library/pdb.html?ref=arabroot.io): `s` (step) للدخول داخل الدالة التي في السطر الحالي بدلاً من تنفيذها كاملة، و`r` (return) لإكمال الدالة الحالية حتى ترجع، و`u` و`d` للصعود والنزول بين الإطارات لترى متغيرات الدالة التي استدعت الدالة الحالية، و`q` للخروج. ويمكنك أيضاً تشغيل أي برنامج تحت المصحح دون تعديله بالأمر `python3 -m pdb orders.py`، فإذا وقع استثناء غير ملتقط يدخل pdb مكان الخطأ نفسه، وتستطيع أن تفحص المتغيرات لحظة الخطأ، وهذا يسمى Post-mortem Debugging.

⚠️

لا تترك `breakpoint()` في الكود الذي يصل إلى السيرفر، والسبب أن البرنامج سوف يتوقف عندها وينتظر أوامر من طرفية لا يراها أحد، فيبدو كأنه معلق. وإذا كنت تريد إيقافها كلها دون حذفها، فالمتغير `PYTHONBREAKPOINT=0` يجعل Python تتجاهلها، كما في توثيق الدالة.

## نقاط التوقف Breakpoints في VS Code

المصحح في الطرفية قوي، ولكن أغلب المبتدئين يستخدمون محرراً مثل VS Code، وفيه مصحح رسومي يعرض كل شيء في وقت واحد. والفكرة نفسها بالضبط: نقطة توقف Breakpoint في سطر، وتشغيل تحت المصحح، ثم فحص المتغيرات والتنفيذ خطوة بخطوة. وسوف نستخدم نسخة JavaScript من الدالة نفسها، لأن مصحح JavaScript وNode.js مدمج في VS Code دون إضافات، أما Python فتحتاج إلى إضافة Python من Microsoft، وهي تستخدم المصحح debugpy كما في [توثيق تصحيح Python في VS Code](https://code.visualstudio.com/docs/python/debugging?ref=arabroot.io)، والواجهة بعد ذلك واحدة.

ولكي نحصل على صورة حقيقية من التشغيل، شغلنا VS Code في المتصفح بواسطة [code-server](https://github.com/coder/code-server?ref=arabroot.io) داخل Container، وهو VS Code نفسه ولكنه يعمل على سيرفر وتفتحه من المتصفح. وأول خطوة هي ملف الإعداد `.vscode/launch.json` الذي يخبر VS Code كيف يشغل برنامجك تحت المصحح:

```json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Run total.js",
      "program": "${workspaceFolder}/total.js"
    }
  ]
}
```

في الإعداد أعلاه لاحظ التالي:

- `"type": "node"` يحدد المصحح المستخدم، وهو هنا مصحح JavaScript المدمج، ولـ Python تكتب `"debugpy"`.
- `"request": "launch"` يعني أن VS Code سوف يشغل البرنامج بنفسه، والخيار الآخر `"attach"` يعني الاتصال ببرنامج يعمل مسبقاً، مثل خدمة تعمل في Container.
- `"program"` هو الملف الذي يبدأ منه التشغيل، و`${workspaceFolder}` هو المجلد المفتوح في المحرر.
- في code-server أضفنا أيضاً `"runtimeExecutable"` يشير إلى نسخة Node.js الموجودة داخل ال Container، أما على جهازك فلا تحتاجه إذا كان `node` في المسار.

وتفاصيل كل الخيارات في [توثيق إعداد التصحيح في VS Code](https://code.visualstudio.com/docs/debugtest/debugging-configuration?ref=arabroot.io)، وإذا لم يوجد الملف أصلاً فإن VS Code يقترح إنشاءه عندما تضغط Run and Debug. وبعد ذلك وضعنا المؤشر على السطر 7 وضغطنا F9 لنضع نقطة توقف، فظهرت دائرة حمراء بجانب رقم السطر، ثم ضغطنا F5 لنبدأ التشغيل تحت المصحح، والصورة التالية هي ما ظهر فعلاً:

![واجهة VS Code بثيم فاتح أثناء التصحيح: الملف total.js مفتوح والسطر 7 const name amount equals orders i مظلل بالأصفر مع علامة نقطة التوقف، وفي اللوحة اليسرى Variables يظهر i يساوي 1 وcustomer يساوي huda وtotal يساوي 0، وتحتها Call Stack فيه totalFor عند 7:28 وanonymous عند 13:22 مع وسم Paused On Breakpoint، وفي الأسفل Debug Console فيه orders[0] والنتيجة huda 120، وشريط التحكم بالتصحيح في أعلى المحرر](https://arabroot.io/content/images/2026/10/debugging-basics-05-vscode-breakpoint.webp)

البرنامج متوقف عند نقطة التوقف في السطر 7، وكل المتغيرات وسلسلة الاستدعاء أمامك

وفي الصورة أعلاه لاحظ التالي:

- السطر 7 مظلل بالأصفر وبجانبه علامة نقطة التوقف، وهذا هو السطر الذي سوف ينفذ بعد ذلك، أي أن البرنامج متوقف قبل تنفيذه.
- لوحة المتغيرات Variables فيها عدة أقسام: `Local: totalFor` فيها معاملات الدالة ومتغيراتها، `customer = 'huda'` و`total = 0` و`orders` بأربعة عناصر، وقسم `Block: totalFor` فيه `i = 1`، والسبب أن `let i` معرف داخل كتلة الحلقة وليس في الدالة كلها. ومن هذه اللوحة وحدها ترى المشكلة: أول دورة و`i` يساوي 1.
- لوحة سلسلة الاستدعاء Call Stack فيها `totalFor` عند `7:28`، والإطار الذي استدعاها `<anonymous>` عند `13:22`، أي سطر `console.log`، ومع وسم `Paused On Breakpoint`. والجملة `Show 7 More: Skipped by skipFiles` تعني أن VS Code أخفى إطارات Node.js الداخلية التي رأيناها في ال Stack Trace، لأنها لا تهمك في الغالب.
- في لوحة Debug Console كتبنا `orders[0]` فظهرت النتيجة `['huda', 120]`، وهذا هو الطلب الذي لن تصل إليه الحلقة أبداً. وهذه اللوحة تقيم أي تعبير في سياق الإطار المختار في Call Stack، وعندما جربنا كتابة التعبير نفسه في محاولة سابقة قبل أن نختار الإطار، ظهرت الرسالة `ReferenceError: orders is not defined`، لأن التقييم حدث خارج الدالة، فاختر الإطار أولاً ثم اكتب.
- الشريط الصغير في أعلى المحرر هو أزرار التحكم: المتابعة Continue وهي F5، وتجاوز السطر Step Over وهي F10، والدخول في الدالة Step Into وهي F11، والخروج منها Step Out وهي Shift+F11، ثم إعادة التشغيل والإيقاف.

وهناك ميزتان في VS Code تجعلان المصحح أقوى من الطباعة بكثير، وكلاهما في [توثيق التصحيح في VS Code](https://code.visualstudio.com/docs/debugtest/debugging?ref=arabroot.io):

- **نقطة التوقف الشرطية Conditional Breakpoint**: بالنقر بالزر الأيمن على مكان النقطة، تكتب شرطاً مثل `name === "huda"`، أو عدد مرات Hit Count، فلا يتوقف البرنامج إلا عند الحالة التي تهمك، وهي نفس فكرة `b 11, name == "huda"` في pdb.
- **نقطة الطباعة Logpoint**: نقطة لا توقف البرنامج، وإنما تطبع رسالة في Debug Console، مثل `i={i} total={total}`، وهذه طباعة دون تعديل الكود، فلا تنسى أي سطر `console.log` خلفك.

وفي JavaScript هناك طريقة ثالثة لوضع نقطة توقف من داخل الكود نفسه، وهي جملة [debugger;](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger?ref=arabroot.io)، فإذا وصل إليها البرنامج وهو يعمل تحت مصحح، سواءً في VS Code أو في أدوات المطور في المتصفح، توقف عندها، وإذا لم يكن هناك مصحح فلا تفعل شيئاً. وNode.js نفسه فيه مصحح في الطرفية مثل pdb بالأمر `node inspect` كما في [توثيق Debugger في Node.js](https://nodejs.org/api/debugger.html?ref=arabroot.io)، ولكن أغلب المطورين يفضلون الواجهة الرسومية في المحرر أو في Chrome DevTools كما في [دليل التصحيح في موقع Node.js](https://nodejs.org/learn/getting-started/debugging?ref=arabroot.io).

## الطباعة print أم المصحح Debugger؟

قد يتساءل البعض: إذا كان المصحح بهذه القوة، فلماذا ما زال المبرمجون المحترفون يستخدمون الطباعة؟ والإجابة أن كل أداة تجيب عن سؤال مختلف، كما يبين الشكل التالي:

![مقارنة من عمودين: عمود الطباعة البرتقالي فيه مخرج total_print.py بالقيم i=1 و2 و3 وhuda 80 ونقاط القوة والضعف، وعمود المصحح الأزرق فيه السطر 7 مع نقطة توقف ولوحة Variables فيها i=1 وtotal=0 وcustomer=huda ولوحة Call Stack ولوحة Debug Console مع orders[0] والنتيجة huda 120](https://arabroot.io/content/images/2026/10/debugging-basics-04-print-vs-debugger.webp)

الطباعة تعطيك أثراً عبر الزمن لما اخترته، والمصحح يعطيك كل شيء في لحظة واحدة

والجدول التالي يلخص متى تختار كل واحدة:

| الموقف                                             | الأداة الأنسب                                 | والسبب                                                         |
| -------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------- |
| نتيجة خاطئة في دالة صغيرة على جهازك                | المصحح                                        | تتوقف وترى كل المتغيرات دون أن تخمن ما تطبعه.                  |
| حلقة طويلة وتريد أن ترى كيف تتغير قيمة عبر الدورات | الطباعة أو Logpoint                           | سطر لكل دورة أوضح من التوقف ألف مرة.                           |
| خطأ يظهر على السيرفر فقط                           | السجلات Logs                                  | لا تستطيع إيقاف خدمة يستخدمها الناس عند نقطة توقف.             |
| خطأ يعتمد على التوقيت، مثل الطلبات المتزامنة       | السجلات مع الوقت                              | التوقف نفسه يغير التوقيت فيختفي الخطأ.                         |
| استثناء غير ملتقط ولا تعرف القيم لحظته             | Post-mortem أو Uncaught Exceptions في VS Code | تدخل المصحح في مكان الخطأ نفسه وتفحص المتغيرات.                |
| الكود كان يعمل وتوقف بعد تعديلات كثيرة             | git bisect                                    | يبحث عن ال Commit الذي أدخل الخطأ بدلاً من البحث في الكود كله. |

ولاحظ في الجدول أن الأداة تعتمد على المكان الذي يقع فيه الخطأ أكثر من نوعه، والقاعدة البسيطة للمبتدئ: على جهازك ابدأ بالمصحح، لأنه يعلمك كيف يتحرك البرنامج فعلاً سطراً سطراً، وهذا وحده درس في البرمجة، وعلى السيرفر اعتمد على السجلات. أما السطر الأخير في الجدول فهو موضوع القسم التالي.

## البحث الثنائي Bisecting: أين دخل الخطأ؟

هناك نوع من الأخطاء لا تساعدك فيه رسالة الخطأ ولا المصحح كثيراً، وهو التراجع Regression: كود كان يعمل الأسبوع الماضي، وتوقف اليوم، وبينهما عشرات التعديلات منك ومن زملائك. وهنا السؤال الأفضل ليس «أين الخطأ في الكود؟» وإنما «أي تعديل أدخل الخطأ؟»، لأنك عندما تعرف ال Commit الذي أدخله، تقرأ تعديلاً واحداً صغيراً بدلاً من المشروع كله. وإذا لم تكن تستخدم Git بعد، فقد شرحناه في مقال [إدارة النسخ وGit للمبتدئين](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

والطريقة البسيطة هي أن ترجع Commit بعد Commit وتختبر كل واحد، ومع 100 Commit قد تحتاج إلى 100 تجربة. والطريقة الأفضل هي البحث الثنائي Binary Search: تختبر ال Commit الذي في المنتصف، فإذا كان سليماً فالخطأ في النصف الأحدث، وإذا كان سيئاً فالخطأ فيه أو في النصف الأقدم، ثم تكرر على النصف المتبقي. وكل تجربة تقسم المجال على اثنين، فمع 100 Commit تكفيك 7 تجارب تقريباً، ومع 1000 تكفيك 10، وهذه هي قوة اللوغاريتم التي شرحناها في مقال [الخوارزميات وتعقيد Big O](https://arabroot.io/articles/%D8%A7%D9%84%D8%AE%D9%88%D8%A7%D8%B1%D8%B2%D9%85%D9%8A%D8%A7%D8%AA-%D9%88%D8%AA%D8%B9%D9%82%D9%8A%D8%AF-big-o-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/). وفي Git هذه الطريقة جاهزة في الأمر [git bisect](https://git-scm.com/docs/git-bisect?ref=arabroot.io).

### git bisect run سطراً بسطر

بنينا في المختبر مستودعاً صغيراً فيه عشرة Commits لملف `total.py`، وفي أحدها غير أحد الزملاء الحلقة لتبدأ من 1 بحجة تجاوز سطر العناوين Header في ملفات CSV، دون أن ينتبه إلى أن الدالة تستلم قائمة ليس فيها عناوين أصلاً. وهذا هو تاريخ المستودع كما يعرضه `git log --oneline`، الأحدث في الأعلى:

```text
8fbf088 Add average_for
260d0ab Describe the orders file
42d3e3a Add header example
dc78d13 Skip the CSV header row
17393ce Document how to run
a75c3ac Loop by index in total_for
f95f4ab Add count_for
dc9e1f3 Add sample orders
0b3dfca Add README
176727b Add total_for
```

ثم كتبنا سكربت فحص صغيراً يستورد الدالة ويتحقق من مجموع هدى، والمهم فيه رمز الخروج Exit Code، لأن `git bisect run` يعتمد عليه: 0 يعني أن ال Commit سليم، وأي رقم من 1 إلى 127 ما عدا 125 يعني أنه سيئ، و125 يعني «تجاوز هذا ال Commit» كما في توثيق الأمر:

```python
# Exit code 0 = good commit, 1 = bad commit (the rule git bisect run follows).
import sys
sys.path.insert(0, ".")
from total import total_for

ORDERS = [("huda", 120), ("omar", 45), ("huda", 80), ("sara", 60)]
got = total_for(ORDERS, "huda")
print("total_for huda =", got)
sys.exit(0 if got == 200 else 1)
```

ثم بدأنا البحث بثلاثة أوامر: `git bisect start` مع ال Commit السيئ أولاً ثم السليم، ثم `git bisect run` مع أمر الفحص، ثم `git bisect reset` للرجوع إلى الفرع بعد الانتهاء:

```bash
first=$(git rev-list --max-parents=0 HEAD)
git bisect start HEAD "$first"
git bisect run python3 /tmp/check.py
git bisect reset
```

والمخرج سوف يكون كما يلي:

```text
Bisecting: 4 revisions left to test after this (roughly 2 steps)
[a75c3acc2df0baba1ca69b7236e0762fccd1faa9] Loop by index in total_for
running 'python3' '/tmp/check.py'
total_for huda = 200
Bisecting: 2 revisions left to test after this (roughly 1 step)
[dc78d1365d99889d8e5de1dd0e46ce24f9f38f2d] Skip the CSV header row
running 'python3' '/tmp/check.py'
total_for huda = 80
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[17393ce9c57db4fc42662e05e0e4014c8993e131] Document how to run
running 'python3' '/tmp/check.py'
total_for huda = 200
dc78d1365d99889d8e5de1dd0e46ce24f9f38f2d is the first bad commit
commit dc78d1365d99889d8e5de1dd0e46ce24f9f38f2d
Author: lab <lab@example.com>
Date:   Wed Oct 7 10:00:00 2026 +0300

    Skip the CSV header row

 total.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commit
```

والشكل التالي يبين ما حدث في كل خطوة:

![خط زمني لعشرة Commits من 176727b الأخضر السليم إلى 8fbf088 الأحمر HEAD، وتحته ثلاث خطوات: الأولى تختبر a75c3ac وتعطي 200 فتخرج النصف الأيسر، والثانية تختبر dc78d13 وتعطي 80 فتخرج ما بعده، والثالثة تختبر 17393ce وتعطي 200، والنتيجة في صندوق أحمر dc78d13 is the first bad commit](https://arabroot.io/content/images/2026/10/debugging-basics-06-bisect.webp)

كل اختبار يقسم المجال المتبقي على اثنين، فثلاثة اختبارات كفت لعشرة Commits

ولنقرأ المخرج سطراً بسطر:

- `Bisecting: 4 revisions left to test after this (roughly 2 steps)`: بين السليم والسيئ ثمانية Commits لم تختبر، فاختار Git واحداً في المنتصف هو `a75c3ac` وأخرجه إلى مجلد العمل، وقدر أنه سوف يحتاج خطوتين تقريباً بعده.
- `running 'python3' '/tmp/check.py'` ثم `total_for huda = 200`: شغل Git سكربت الفحص على هذا ال Commit، والنتيجة صحيحة ورمز الخروج 0، فكل ما قبل `a75c3ac` سليم وخرج من البحث.
- الخطوة الثانية اختارت `dc78d13` وأعطت `80`، فهذا ال Commit سيئ، والخطأ فيه أو في ما بينه وبين آخر Commit سليم.
- الخطوة الثالثة اختارت `17393ce`، وهو ال Commit الوحيد بين `a75c3ac` و`dc78d13`، وأعطى `200`، فهو سليم.
- والنتيجة: `dc78d13... is the first bad commit`، أي أول Commit سيئ، مع رسالته `Skip the CSV header row` وأنه غير سطراً واحداً في `total.py`. والآن تفتح هذا التعديل بالأمر `git show dc78d13` فترى السطر الذي غير `range(len(orders))` إلى `range(1, len(orders))`، وتعرف أيضاً لماذا غيره صاحبه، فتصلح المشكلة الحقيقية دون أن تعيد مشكلته هو.

وثلاثة اختبارات هنا قد تبدو قليلة الفائدة مع عشرة Commits، ولكن الفرق يكبر مع حجم المشروع، ولهذا يعتمد مطورو نواة Linux على هذه الأداة لإيجاد التعديل الذي أدخل التراجع بين إصدارين، وفي توثيق النواة صفحة كاملة عن الطريقة بعنوان [Bisecting a bug](https://docs.kernel.org/admin-guide/bug-bisect.html?ref=arabroot.io)، لأن بين إصدارين من النواة آلاف ال Commits، والبحث الثنائي يحولها إلى عدد قليل من عمليات البناء والاختبار.

### البحث الثنائي في البيانات Data Bisecting

والفكرة نفسها تعمل على البيانات وليس على ال Commits فقط. فلنفرض أن البرنامج الأول، قبل أن نضيف رقم السطر إلى الرسالة، يفشل على ملف من 5000 طلب، وفي سطر ما فاصلة زائدة داخل المبلغ. وبدلاً من أن تقرأ الملف كله بعينك، تجرب البرنامج على النصف الأول من الأسطر، فإذا نجح فالسطر الخاطئ في النصف الثاني، ثم تكرر. وكتبنا سكربتاً يفعل ذلك، وزرع سطراً خاطئاً في مكان عشوائي، والمخرج سوف يكون كما يلي:

```text
5000 lines, works: False
try  1: lines 2501..5000
try  2: lines 3751..5000
try  3: lines 4376..5000
try  4: lines 4376..4687
try  5: lines 4376..4531
try  6: lines 4376..4453
try  7: lines 4415..4453
try  8: lines 4434..4453
try  9: lines 4434..4443
try 10: lines 4434..4438
try 11: lines 4434..4435
try 12: lines 4434..4434
bad line 4434: 'customer_x,12,5' after 12 tries (planted at line 4434)
```

اثنتا عشرة تجربة كفت لإيجاد سطر واحد بين 5000، والسبب أن 2 أس 12 يساوي 4096 و2 أس 13 يساوي 8192، فالعدد اللازم يقارب اللوغاريتم الثنائي لعدد الأسطر. والسطر نفسه `customer_x,12,5` فيه فاصلة داخل المبلغ، وهذا يحدث عندما يكتب أحدهم المبلغ بفاصلة عشرية كما في بعض الإعدادات الإقليمية، أي 12,5 بدلاً من 12.5، فيصبح السطر ثلاثة أجزاء بدلاً من جزأين. وهذه الطريقة تفيدك أيضاً عندما يفشل برنامج على ملف إعداد طويل، أو على مجموعة كبيرة من الملفات، أو حتى على قائمة إضافات Extensions في المتصفح، فتعطل النصف وتجرب.

### ماذا لو لم يعمل Commit في منتصف البحث؟

في المشاريع الحقيقية قد يختار `git bisect` Commit لا يعمل أصلاً لسبب لا علاقة له بالخطأ الذي تبحث عنه، مثل خطأ في البناء أصلحه أحدهم في ال Commit التالي. فإذا اعتبرته سيئاً فسوف يشير البحث إلى Commit خاطئ، وإذا اعتبرته سليماً فكذلك. ولهذا وضع Git رمز الخروج الخاص 125، فإذا رجع سكربت الفحص به، يتجاوز Git هذا ال Commit ويختار جاراً له. والقاعدة في سكربت الفحص أن تفرق بين «الاختبار فشل» و«لم أستطع الاختبار أصلاً»، فإذا فشل استيراد الملف مثلاً ارجع 125 وليس 1.

وقبل أن تبدأ أي بحث ثنائي تأكد من شيئين: أن ال Commit الذي تسميه سليماً سليم فعلاً، فشغل الفحص عليه يدوياً، وأن الفحص يعطي النتيجة نفسها في كل مرة، لأن الاختبار الذي ينجح مرة ويفشل مرة، مثل اختبار يعتمد على الوقت أو الشبكة، سوف يقود البحث إلى Commit عشوائي.

## ماذا يحدث إذا ابتلعت الخطأ Swallowing؟

رأينا حتى الآن كيف تساعدك رسالة الخطأ، والآن نرى ماذا يحدث عندما تخفيها. فكثير من المبتدئين، بعد أن يتعبوا من الرسائل الحمراء، يضعون الكود في `try` ويطبعون رسالة قصيرة أو لا يطبعون شيئاً. وكتبنا سكربتاً يحمل ملف الهللات نفسه ويلتقط الخطأ بثلاث طرق، الأولى تطبع الرسالة فقط، والثانية تستخدم `logging.exception`، والثالثة `pass`، وبعدها يطبع البرنامج `done`:

```python
try:
    load_orders("orders-decimal.txt")
except ValueError as e:
    if mode == "print":
        print("error:", e)                       # message only: no type, no file, no line
    elif mode == "log":
        logging.exception("loading orders failed")  # message plus the full traceback
    elif mode == "pass":
        pass                                     # nothing at all
print("done")
```

والمخرج للطرق الثلاث سوف يكون كما يلي:

```text
--- print
error: invalid literal for int() with base 10: '45.5'
done
--- log
done
ERROR loading orders failed
Traceback (most recent call last):
  File "/lab/swallow.py", line 10, in <module>
    load_orders("orders-decimal.txt")
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File "/lab/orders.py", line 14, in load_orders
    orders.append(parse_line(line))
                  ~~~~~~~~~~^^^^^^
  File "/lab/orders.py", line 7, in parse_line
    return customer, int(amount)
                     ~~~^^^^^^^^
ValueError: invalid literal for int() with base 10: '45.5'
--- pass
done
```

في المخرج أعلاه لاحظ التالي:

- طريقة `print("error:", e)` أبقت الرسالة وحذفت كل شيء آخر: لا يوجد نوع الخطأ `ValueError`، ولا اسم الملف، ولا رقم السطر، ولا سلسلة الاستدعاء. وفي برنامج فيه عشرات الأماكن التي تحول نصاً إلى رقم، هذه الرسالة لا تقول لك أين تبحث.
- طريقة `logging.exception` كتبت رسالتنا ثم ال Traceback كاملاً، وهي الطريقة الصحيحة عندما تريد أن يكمل البرنامج بعد الخطأ مع الاحتفاظ بكل المعلومات، كما في [توثيق logging.exception](https://docs.python.org/3/library/logging.html?ref=arabroot.io#logging.exception).
- طريقة `pass` لم تترك أي أثر، فالبرنامج طبع `done` كأن شيئاً لم يحدث، والطلبات لم تحمل. وهذه أسوأ الطرق على الإطلاق، لأن الخطأ لم يختف، وإنما انتقل إلى مكان آخر في البرنامج حيث سيظهر بشكل لا علاقة له بسببه، مثل تقرير فارغ.

ولاحظ شيئاً غريباً في مخرج طريقة `log`: كلمة `done` ظهرت قبل رسالة الخطأ، مع أن البرنامج طبعها بعده. وهذه مشكلة حقيقية واجهتنا عند التشغيل، وسببها أن `print` يكتب إلى المخرج القياسي stdout، والتسجيل يكتب إلى مخرج الأخطاء stderr، وعندما لا يكون المخرج طرفية تفاعلية، كما هنا داخل Docker مع إعادة التوجيه، فإن Python تجمع ما تكتبه إلى stdout في ذاكرة مؤقتة Buffer وتكتبه دفعة واحدة، أما stderr فيكتب فوراً، فيختلط الترتيب. وقد يضيع منك وقت طويل وأنت تظن أن البرنامج نفذ الأسطر بترتيب غير الذي كتبته. والحل إذا كان الترتيب يهمك هو أن تشغل Python بالخيار `-u`، أو تضبط المتغير `PYTHONUNBUFFERED=1` كما في [توثيق خيارات سطر الأوامر](https://docs.python.org/3/using/cmdline.html?ref=arabroot.io)، أو أن تكتب `print(..., flush=True)`، والأفضل أن ترسل كل رسائل التصحيح إلى مكان واحد عبر مكتبة التسجيل. وسبب وجود هذه الذاكرة المؤقتة أصلاً هو تقليل استدعاءات النظام System Calls المكلفة، كما شرحنا في مقال [فضاء النواة والمستخدم واستدعاءات النظام](https://arabroot.io/articles/%D9%81%D8%B6%D8%A7%D8%A1-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D8%A9-%D9%88%D9%81%D8%B6%D8%A7%D8%A1-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%AE%D8%AF%D9%85-%D9%88%D8%A7%D8%B3%D8%AA%D8%AF%D8%B9%D8%A7%D8%A1%D8%A7%D8%AA-%D8%A7%D9%84%D9%86%D8%B8%D8%A7%D9%85/).

🛑

لا تكتب `except: pass` أو `catch (e) {}` فارغة أبداً لتسكت خطأ لا تفهمه، والسبب أنك بذلك تحذف الدليل الوحيد على المشكلة، وتنقلها إلى مكان أبعد يصعب فيه التصحيح أكثر. وإذا كان تجاهل الخطأ مقصوداً فعلاً، فالتقط النوع المحدد فقط واكتب تعليقاً يشرح لماذا، كما فصلنا في قسم الأخطاء الشائعة في [مقال كيف تتعامل لغات البرمجة مع الأخطاء](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1/#mistakes).

## أخطاء شائعة Common Mistakes في التصحيح

- **قراءة ال Traceback من أوله**: في Python تبدأ من آخر سطر، وفي Node.js من أول سطر بعد الكود المطبوع، وفي الحالتين تبحث عن أول إطار من ملفاتك أنت وليس من المكتبات.
- **التوقف عند نوع الخطأ**: `ValueError` وحده لا يكفي، فقد رأينا نوعين مختلفين تماماً من المشاكل بالنوع نفسه، والرسالة والقيمة التي فيها هي التي تحدد السبب.
- **تغيير الكود عشوائياً**: تغير سطراً، ثم آخر، ثم ثالثاً، حتى يختفي الخطأ، فلا تعرف أي تغيير أصلحه، وقد يكون أحدها أدخل خطأ جديداً. غير شيئاً واحداً في كل مرة، وارجع عنه إذا لم يثبت الفرضية.
- **إصلاح العرض بدلاً من السبب**: مثل أن تضيف `if amount is None: amount = 0` لتختفي رسالة الخطأ، بينما السبب سطر فارغ يجب تجاوزه، أو بيانات خاطئة يجب رفضها.
- **الوثوق بأن الكود القديم سليم**: الخطأ في أغلب الأحيان في الكود الذي «لا يمكن أن يكون فيه خطأ»، لأنه الكود الذي لم تفحصه. اختبر الفرضية حتى عندما تكون متأكداً.
- **نسيان أدوات التصحيح في الكود**: سطور `print` و`console.log` و`breakpoint()` و`debugger;` تصل إلى الإنتاج، والأخيرتان قد توقفان البرنامج أو تكشفان بيانات في السجلات.
- **إعادة صياغة رسالة الخطأ عند السؤال**: عندما تطلب المساعدة لا تكتب «يظهر لي خطأ في التحويل»، وإنما انسخ آخر سطر كما هو، والإطارات التي من كودك، وإصدار اللغة، لأن الرسالة الحرفية هي ما يبحث عنه من يساعدك.
- **البحث عن الرسالة كاملة بمساراتك**: إذا بحثت في الإنترنت عن السطر كاملاً مع `/home/ahmed/project/orders.py` فلن تجد شيئاً، فاحذف المسارات والقيم الخاصة بك وابحث عن نوع الخطأ والجزء العام من الرسالة.

## قائمة تحقق Checklist قبل أن تطلب المساعدة

قبل أن تكتب لزميلك أو في منتدى، مر على هذه القائمة، وغالباً سوف تجد الحل قبل أن تصل إلى آخرها:

| السؤال                                                | لماذا                                                                   |
| ----------------------------------------------------- | ----------------------------------------------------------------------- |
| هل أستطيع إعادة إنتاج الخطأ بأمر واحد؟                | بدونها لن تعرف هل أصلحته.                                               |
| هل قرأت آخر سطر من ال Traceback حتى آخر كلمة؟         | الرسالة قد تحتوي على القيمة الخاطئة أو على الحل مثل Did you mean.       |
| هل وجدت آخر إطار من ملفاتي أنا؟                       | هذا غالباً المكان الذي تبدأ منه.                                        |
| هل كتبت المتوقع والنتيجة بدقة؟                        | «المتوقع 200 والنتيجة 80» تقترح فرضية، و«النتيجة خاطئة» لا تقترح شيئاً. |
| هل صغرت المشكلة إلى أصغر كود وبيانات تظهرها؟          | نصف المشاكل تحل أثناء التصغير.                                          |
| هل اختبرت فرضيتي بالطباعة أو المصحح بدلاً من التخمين؟ | الفرضية التي لم تختبر رأي وليست معلومة.                                 |
| هل كان الكود يعمل سابقاً؟ هل جربت git bisect؟         | معرفة التعديل الذي أدخل الخطأ أسرع من البحث في المشروع كله.             |
| هل معي الإصدارات: اللغة والمكتبات ونظام التشغيل؟      | كثير من الأخطاء تظهر في إصدار دون آخر.                                  |
| بعد الإصلاح: هل كتبت اختباراً يفشل مع الكود القديم؟   | حتى لا يرجع الخطأ نفسه بعد أشهر.                                        |

## تدريب عملي

- في نسخة Python الأولى من البرنامج، أضف إلى ملف الطلبات سطراً فيه اسم العميل فقط دون فاصلة، مثل `nora`، وشغل البرنامج، واقرأ ال Traceback بالترتيب الذي شرحناه، ثم قارن رسالته برسالة السطر الفارغ: ما الذي تغير في الرقم بين القوسين؟
- ضع في VS Code نقطة توقف شرطية في دالة `totalFor` بالشرط `name === "sara"`، وشغل البرنامج، وتحقق من قيمة `total` لحظة التوقف، ثم حول النقطة إلى Logpoint يطبع `i` و`total` في كل دورة.

## الخلاصة

- التصحيح Debugging هو إيجاد سبب الفرق بين ما تريده من البرنامج وما يفعله، وإزالة السبب وليس العرض، والكلمة أقدم من قصة فراشة Mark II في 9 سبتمبر 1947.
- في Python اقرأ ال Traceback من آخر سطر، ثم آخر إطار، ثم اصعد، وعلامات `^^^` منذ Python 3.11 تحدد التعبير الذي فشل، و`Did you mean` منذ 3.10 تقترح الاسم الصحيح.
- عندما تلتقط خطأ وترمي غيره استخدم `raise ... from e`، فيظهر السبب الأصلي مع رسالتك التي فيها الملف ورقم السطر.
- في Node.js الرسالة ومكان الخطأ في الأعلى، وV8 يجمع عشرة إطارات افتراضياً، و`at async` تريك سلسلة `await`.
- أخطر الأخطاء التي لا ترمي أي استثناء، مثل `NaN` من سطر فارغ في نهاية الملف، ولذلك تحقق من البيانات عند دخولها وأوقف البرنامج برسالة واضحة.
- الطباعة سريعة وتعمل في كل مكان، والمصحح، سواءً pdb أو VS Code، يريك كل المتغيرات وسلسلة الاستدعاء دون تعديل الكود، ونقاط التوقف الشرطية وLogpoints تجمع بين الاثنين.
- `git bisect run` وجد ال Commit الذي أدخل الخطأ بين عشرة في ثلاثة اختبارات، والبحث الثنائي في البيانات وجد سطراً خاطئاً بين 5000 في 12 تجربة.
- ابتلاع الخطأ بـ `pass` أسوأ الطرق، و`logging.exception` يحفظ ال Traceback كاملاً، وانتبه إلى أن stdout وstderr قد يظهران بترتيب مختلف عن ترتيب الكود.

وإذا أردت أن تكمل من هنا، فهذه مقالات مرتبطة بالموضوع:

[![](https://arabroot.io/content/images/2026/10/error-handling-cover-cover.webp)دليلكيف تتعامل لغات البرمجة مع الأخطاء؟لماذا ترمي Java وPython الاستثناءات بينما ترجع Go وRust الخطأ كقيمة، وكيف تمنع الخطأ من أن يختفي بصمت في كودك.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1/)[![](https://arabroot.io/content/images/2026/10/memory-basics-cover-cover.webp)دليلالذاكرة للمبرمجين: كيف تعمل ال Stack وال Heapلماذا تتغير قائمة لم تطلب تغييرها، وكيف تتسرب الذاكرة في لغة فيها جامع قمامة Garbage Collector، بأمثلة حقيقية من عدة لغات.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86-stack-%D9%88-heap-%D9%88%D8%A7%D9%84%D9%85%D8%A4%D8%B4%D8%B1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/recursion-cover-cover.webp)دليلالاستدعاء الذاتي Recursion: كيف تفكر فيه ومتى تتجنبهكيف تكتب دالة تستدعي نفسها لشجرة تعليقات أو مجلدات، وما الذي يحدث في ال Stack، وإلى أي عمق تصل Python وNode.js بالقياس، ومتى تكون الحلقة هي الحل الأنسب.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%AF%D8%B9%D8%A7%D8%A1-%D8%A7%D9%84%D8%B0%D8%A7%D8%AA%D9%8A-recursion-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/files-io-streams-cover-cover.webp)دليلالملفات Files والتدفقات Streams: كيف يقرأ برنامجك ملفاً؟لماذا يظهر النص العربي في ملف CSV من Excel مشوهاً، ولماذا يتوقف تقرير بسيط عندما يكبر ملف السجلات، وكيف تقرأ الملفات وتكتبها بأمان في Python وNode.js بقياسات حقيقية.](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%AA%D8%AF%D9%81%D9%82%D8%A7%D8%AA-files-%D9%88-streams-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/logging-basics-cover-cover.webp)دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/testing-types-cover-cover.webp)دليلأنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwrightما الذي يكشفه كل نوع من الاختبارات وما الذي يفوته، وكم اختباراً تكتب من كل نوع، وأين يعمل كل منها في خط CI.](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/)[![](https://arabroot.io/content/images/2026/10/git-basics-cover-cover.webp)دليلإدارة النسخ وGit للمبتدئين: لماذا نحتاجها وكيف تعمللماذا تضيع الملفات بأسماء final\_v2 ويمسح الزملاء عمل بعضهم، وكيف يحل Git ذلك على مشروع حقيقي من أول commit.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/algorithms-bigo-cover-cover.webp)دليلBig-O للمبتدئين: لماذا يبطؤ كودك عندما تكبر البياناتلماذا يعمل الكود بسرعة على مئة صف ثم يتوقف على مئة ألف، وكيف تقرأ Big-O وتكتشف الخوارزمية البطيئة في كودك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AE%D9%88%D8%A7%D8%B1%D8%B2%D9%85%D9%8A%D8%A7%D8%AA-%D9%88%D8%AA%D8%B9%D9%82%D9%8A%D8%AF-big-o-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال اعتماداً على توثيق Python وNode.js وV8 وVS Code وgit، مع مختبر حقيقي داخل Docker على Python 3.13.17 وNode.js v22.23.4 وgit 2.47.3، وصورة من جلسة تصحيح حقيقية في code-server 4.144.0.