كيف تُصَحِّح أخطاء JavaScript مثل المحترفين
تعلّم JavaScript شيء، وتعلّم تصحيح أخطائها بسرعة وذكاء شيء آخر تمامًا. كثيرون يكتبون كودًا “يبدو صحيحًا”، ثم يقضون ساعة أو ساعتين في مطاردة خطأ صغير: متغير غير معرّف، Promise لم يُنتظر، DOM لا يتحدث كما يجب، أو حالة تتغير في مكان غير متوقع. هنا تظهر مهارة الـ Debugging كأحد أهم الفوارق بين مطوّر عادي ومطوّر واثق من نفسه.
في هذا المقال، لن نكتفي بشرح الأدوات، بل سنبني عقلية عملية تساعدك على فهم المشكلة من جذورها. ستتعلم كيف تقرأ الرسائل الخطأ، وكيف تستخدم console وDevTools بذكاء، وكيف تتعامل مع الأخطاء في المتصفح وNode.js، وكيف تكتشف المشاكل الصامتة التي لا تُظهر لك أي رسالة واضحة. وسنضيف أمثلة واقعية وجداول تساعدك على الرجوع بسرعة عندما تتعطل الأمور.
لماذا يبدو Debugging صعبًا في JavaScript؟
السبب ليس أن JavaScript “معقدة جدًا”، بل لأنها لغة مرنة إلى درجة كبيرة. هذه المرونة الجميلة تأتي أحيانًا بثمن: أخطاء غير متوقعة، تحويلات نوعية صامتة، سلوك غير واضح في this، وتوقيت غير مباشر بسبب الـ event loop والـ asynchronous code.
أحيانًا يكون الخطأ نفسه بسيطًا جدًا، لكن المشكلة الحقيقية هي أنك لا تعرف أين تنظر أولًا. لهذا السبب، المحترف لا يبدأ بالتخمين، بل يبدأ بطريقة منهجية:
يحدد المشكلة بدقة.
يعيد إنتاجها.
يضيّق النطاق.
يقرأ الرسالة أو الأثر.
يختبر فرضية واحدة في كل مرة.
يصل للحل ثم يتحقق منه.
هذه هي الفكرة الأساسية: لا تلاحق الخطأ، بل حاصر السلوك.
العقلية الصحيحة قبل الأدوات
قبل أي console.log أو breakpoint، اسأل نفسك:
هل المشكلة تحدث دائمًا أم أحيانًا؟
هل هي مرتبطة ببيانات معينة؟
هل ظهرت بعد تعديل محدد؟
هل هي في المتصفح فقط أم في Node.js أيضًا؟
هل هي خطأ في المنطق أم خطأ في الواجهة أم مشكلة أداء؟
الفرق بين مطوّر مرتبك ومطوّر محترف هو أن الثاني لا يقول: “الكود لا يعمل”، بل يقول: “الزر يضغط، لكن الطلب لا يُرسل إلا بعد إعادة تحميل الصفحة”. هذا الوصف الدقيق يختصر نصف الحل.
القاعدة الذهبية الأولى: أعد إنتاج الخطأ
أحد أكبر الأخطاء أثناء التصحيح هو محاولة إصلاح شيء لا تستطيع إعادة إنتاجه. قبل أي خطوة، اجعل المشكلة قابلة للتكرار. إن كانت تظهر فقط بعد تسلسل معين من الأحداث، حاول اختصاره إلى أقل مثال ممكن.
مثال على إعادة إنتاج مشكلة
function calculateDiscount(price, discount) {
return price - price * discount;
}
console.log(calculateDiscount(100, "0.2"));
قد يبدو هذا صحيحًا للوهلة الأولى، لكن النتيجة قد تكون غير متوقعة بسبب أن discount نص وليس رقمًا في بعض الحالات، وقد تنتج سلوكيات غريبة بحسب السياق. إعادة الإنتاج هنا تعني أنك تعزل المدخلات وتفحص النوع والقيمة معًا.
كيف تقرأ رسائل الخطأ بذكاء
رسالة الخطأ ليست عدوك. هي إشارة. والمطور الجيد لا يقرأ الرسالة فقط، بل يقرأ:
نوع الخطأ
الملف
السطر
الـ stack trace
السياق الذي أدى إليه
أشهر أنواع الأخطاء
نوع الخطأ | المعنى | مثال شائع | كيف تتعامل معه |
|---|---|---|---|
| استخدام متغير غير معرّف |
| تحقق من الاسم والنطاق |
| تنفيذ عملية غير صالحة على نوع بيانات |
| تحقق من القيمة قبل الاستخدام |
| خطأ في الصياغة | قوس ناقص أو فاصلة مفقودة | راجع البنية |
| قيمة خارج النطاق المقبول |
| افحص التكرار أو العمق |
| خطأ في التعامل مع URI |
| تحقق من المدخلات |
| مرتبط بـ | نادر الاستخدام | تجنبه غالبًا |
مثال على TypeError
const user = null;
console.log(user.name);
الخطأ هنا واضح عند القراءة: لا يمكنك الوصول إلى name على قيمة null. الحل ليس فقط “إصلاح السطر”، بل فهم لماذا أصبحت user فارغة من الأساس.
حل آمن:
const user = null;
console.log(user?.name);
لكن انتبه: optional chaining لا يُصلح المنطق، بل يمنع الانفجار فقط. إن كانت القيمة يجب أن تكون موجودة، فالأفضل معالجة السبب.
استخدم console بذكاء، لا بعشوائية
كثيرون يضعون عشرات console.log ثم ينسون إزالتها. هذا يربكهم أكثر مما يساعدهم. استخدم الـ console بطريقة منظمة.
ما الذي تستخدمه ومتى؟
الأداة | الاستخدام المناسب |
|---|---|
| عرض قيمة عادية |
| تنبيه غير قاتل |
| خطأ واضح |
| عرض مصفوفات أو كائنات بشكل منظم |
| معرفة مسار الاستدعاء |
| تنظيم السجل في مجموعات |
| التحقق من شرط معيّن |
مثال عملي
const users = [
{ id: 1, name: "Aya", active: true },
{ id: 2, name: "Yassine", active: false },
{ id: 3, name: "Sara", active: true },
];
console.table(users);
هذا أفضل بكثير من طباعة كل عنصر على سطر منفصل. ستقرأ البيانات بسرعة وتلاحظ أي شيء غير طبيعي.
مثال على console.trace()
function stepOne() {
stepTwo();
}
function stepTwo() {
console.trace("Trace from stepTwo");
}
stepOne();
هذه الطريقة مفيدة جدًا عندما تريد معرفة من استدعى دالة معيّنة، خاصة في التطبيقات الكبيرة.
استخدم DevTools كأنها مختبر، لا كنافذة عرض
أغلب المطورين يعرفون أن DevTools موجودة، لكن قلة فقط تستخدمها بالكامل. لو أتقنتها، ستوفر ساعات كل أسبوع.
أهم الأجزاء في DevTools
الجزء | فائدته |
|---|---|
Elements | فحص DOM وCSS |
Console | تنفيذ أوامر ومراجعة الأخطاء |
Sources | وضع breakpoints وتتبع التنفيذ |
Network | مراقبة الطلبات والاستجابات |
Application | التخزين المحلي، الكوكيز، الـ storage |
Performance | تحليل البطء |
Memory | تتبع تسرب الذاكرة |
كيف تستخدم Breakpoints بشكل احترافي
الـ breakpoint أقوى من مئات console.log. بدل أن تطبع كل شيء، أوقف التنفيذ في اللحظة المناسبة.
متى تضيف breakpoint؟
قبل المكان الذي تعتقد أن القيمة تتغير فيه
داخل loop مشبوه
قبل أو بعد
awaitعند استقبال بيانات من API
عند click handler لا يعمل كما يجب
مثال
function processOrder(order) {
const total = order.items.reduce((sum, item) => sum + item.price, 0);
return total * 1.2;
}
إذا كانت النتيجة خاطئة، ضع breakpoint داخل الدالة وتحقق من:
قيمة
orderمحتوى
order.itemsقيمة
item.priceهل هي رقم أم نص؟
هل هناك عنصر ناقص؟
Debugging للـ DOM والواجهة
أحيانًا يكون الكود منطقيًا، لكن الواجهة لا تتغير. هنا لا يكفي فحص JavaScript فقط. قد تكون المشكلة في:
selector خاطئ
العنصر غير موجود بعد
الحدث لم يُربط أصلاً
CSS يخفي العنصر
تحديث الحالة يحدث لكن الrender لم يتم
DOM تغيّر بعد تحميل الصفحة
مثال شائع
document.querySelector("#saveBtn").addEventListener("click", function () {
document.querySelector("#status").textContent = "Saved!";
});
إذا لم يعمل هذا، افحص:
console.log(document.querySelector("#saveBtn"));
console.log(document.querySelector("#status"));
إذا كانت النتيجة null، فالمشكلة ليست في addEventListener بل في العنصر نفسه أو توقيت تحميله.
مشكلة التوقيت
document.querySelector("#saveBtn").addEventListener("click", () => {
console.log("Clicked");
});
إذا كان السكربت يعمل قبل وجود العنصر في الصفحة، ستحصل على خطأ. الحل:
document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#saveBtn").addEventListener("click", () => {
console.log("Clicked");
});
});
Debugging للأحداث Events
الأحداث في JavaScript تبدو سهلة، لكنها من أكثر مناطق الأخطاء شيوعًا. أحيانًا تربط الحدث بعنصر خطأ، أو تستخدم event delegation بشكل غير صحيح، أو تنسى أن الزر داخل form يسبب submit.
مثال على مشكلة submit
document.querySelector("button").addEventListener("click", () => {
console.log("Button clicked");
});
إذا كان الزر داخل نموذج، فقد يحدث submit ويعيد تحميل الصفحة بسرعة فتظن أن الكود لم يعمل.
حل محتمل:
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
console.log("Form submitted without reload");
});
جدول سريع لفهم مشاكل الأحداث
المشكلة | السبب المحتمل | طريقة الفحص |
|---|---|---|
الحدث لا يعمل | العنصر غير موجود | فحص selector |
يعمل ثم يختفي | إعادة تحميل الصفحة | فحص submit |
يعمل على عنصر ويعطل على آخر | event delegation خطأ | فحص |
يحدث مرتين | listener مكرر | فحص التسجيل أكثر من مرة |
Debugging في الـ Async JavaScript
الـ async code هو المكان الذي يضيع فيه الكثير من الوقت. لأن المشكلة لا تظهر دائمًا في السطر الذي تتوقعه، بل بعده بزمن.
المشهد الشائع
fetch("/api/user")
.then((response) => response.json())
.then((data) => {
console.log(data.name);
});
إذا لم يعمل، فالمشكلة قد تكون في:
الرابط نفسه
الاستجابة ليست JSON
الخادم يرجع خطأ
البيانات لا تحتوي
namethenلا يحتويreturnفي مكان ما
مثال باستخدام async/await
async function loadUser() {
try {
const response = await fetch("/api/user");
const data = await response.json();
console.log(data.name);
} catch (error) {
console.error("Failed to load user:", error);
}
}
هذا أفضل من التابع الطويل أحيانًا، لأنه يجعل المسار أوضح.
أين تضع breakpoint هنا؟
قبل
fetchبعد
responseقبل
response.json()داخل
catch
مشكلة شائعة جدًا: نسيان await
async function getData() {
const data = fetch("/api/data");
console.log(data);
}
هنا data ليست البيانات نفسها، بل Promise.
الصحيح:
async function getData() {
const data = await fetch("/api/data");
console.log(data);
}
جدول مختصر لفهم مشاكل async
العرض | السبب المحتمل | الحل |
|---|---|---|
| نسيت | استخدم |
خطأ بعد وقت | الاستدعاء متأخر | راقب stack trace |
البيانات فارغة | response غير صحيح | افحص |
لا يوجد catch | لم تضف معالجة أخطاء | أضف |
فحص الاستجابة من الـ API
عندما تستدعي API، لا تفترض أن كل شيء رجع كما تريد. افحص دائمًا:
response.okresponse.statusresponse.headersصيغة JSON
البنية الفعلية للبيانات
مثال جيد
async function fetchUsers() {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
console.log(data);
}
بهذه الطريقة، لن تتعامل مع خطأ الشبكة والخطأ المنطقي كأنهما شيء واحد.
Debugging للـ Scope والنطاق
من أخطاء JavaScript المحبطة أن المتغير “يبدو موجودًا” في مكان، لكنه غير متاح في مكان آخر.
مثال
function test() {
if (true) {
let message = "Hello";
}
console.log(message);
}
هذا سيفشل لأن message محصورة داخل block.
جدول فهم النطاق
الكلمة المفتاحية | النطاق | ملاحظات |
|---|---|---|
| function scope | قد يسبب مشاكل قديمة |
| block scope | أفضل في أغلب الحالات |
| block scope | للثوابت المرجعية |
مثال آخر على نطاق غير متوقع
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
هذا سيطبع:0, 1, 2
لكن لو استعملت var قد تتغير النتيجة:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
هنا غالبًا ستحصل على 3 ثلاث مرات.
فهم this عندما يبدأ كل شيء بالارتباك
this من أشهر أسباب الضياع في JavaScript. لا تتعامل معه كمفهوم محفوظ، بل كقيمة تُحدد حسب طريقة الاستدعاء.
مثال بسيط
const user = {
name: "Hassan",
sayName() {
console.log(this.name);
},
};
user.sayName();
هنا this تشير إلى user.
لكن عند فصل الدالة:
const say = user.sayName;
say();
قد لا تكون this كما تتوقع.
كيف تصحح هذا؟
استخدم
.bind()استخدم arrow function في المواضع المناسبة
لا تفترض أن
thisثابتة
كيف تكتشف المشاكل المنطقية
ليس كل bug خطأً في الكود. أحيانًا الكود صحيح نحويًا، لكنه خاطئ منطقيًا.
مثال
function isAdult(age) {
return age > 18;
}
هذا قد يكون خطأ إذا كان تعريف البالغ يبدأ من 18 شاملًا. الصحيح:
function isAdult(age) {
return age >= 18;
}
هذا النوع من الأخطاء لا يلتقطه JavaScript. لذلك تحتاج إلى اختبار منطقي.
اسأل نفسك دائمًا:
هل الشرط يعكس المطلوب فعلًا؟
هل الحدّ الأدنى أو الأعلى متضمن؟
هل المقارنة نصية أم رقمية؟
هل الفرع else منطقي فعلًا؟
هل القيمة الافتراضية مناسبة؟
Debugging أنواع البيانات
JavaScript أحيانًا تبتسم لك ثم تفاجئك بتحويلات غريبة.
مثال
console.log("5" + 1);
النتيجة:"51"
لكن:
console.log("5" - 1);
النتيجة:4
لماذا؟
لأن + قد يجري concatenation، بينما - يجبر التحويل إلى رقم.
جدول مهم جدًا
التعبير | النتيجة | السبب |
|---|---|---|
|
| دمج نصوص |
|
| تحويل رقمي |
|
|
|
|
|
|
|
| قيمة غير رقمية |
كيف تحمي نفسك؟
const value = Number(inputValue);
if (Number.isNaN(value)) {
throw new Error("Invalid number");
}
Debugging الأداء Performance
أحيانًا الكود لا ينهار، لكنه بطيء. والبطء نفسه bug.
علامات مشكلة الأداء
الصفحة تتجمد
التمرير بطيء
النقر يتأخر
الذاكرة تزداد باستمرار
العمليات المتكررة تستهلك CPU
أسباب شائعة
السبب | المثال | التأثير |
|---|---|---|
حلقة كبيرة | loop داخل loop | بطء ملحوظ |
إعادة render متكرر | state updates كثيرة | واجهة بطيئة |
DOM queries مكررة |
| استهلاك غير ضروري |
listeners مكررة | event registration زائد | مشاكل ذاكرة |
صور/بيانات ضخمة | تحميل غير محسوب | بطء في البداية |
مثال غير جيد
for (let i = 0; i < 10000; i++) {
const element = document.querySelector("#box");
element.textContent = i;
}
هنا أنت تبحث عن العنصر 10000 مرة بلا داعٍ.
الأفضل:
const element = document.querySelector("#box");
for (let i = 0; i < 10000; i++) {
element.textContent = i;
}
Debugging تسرب الذاكرة Memory Leaks
الذاكرة لا “تختفي” دائمًا بمجرد انتهاء استخدامك للكائنات. أحيانًا يبقى المرجع موجودًا، فيستمر المتصفح أو Node.js في الاحتفاظ بها.
أسباب شائعة
listeners لا تُزال
timers تبقى فعالة
arrays أو caches تكبر بلا حدود
closures تحتفظ ببيانات كبيرة
DOM elements محذوفة لكنها ما زالت مرجعية
مثال
let dataCache = [];
function addData(item) {
dataCache.push(item);
}
إذا استمر dataCache في النمو دون تنظيف، فقد تتحول المشكلة إلى استهلاك ذاكرة حقيقي.
ماذا تفعل؟
راقب النمو
أزل listeners عند الحاجة
صفّر timers
نظّف المراجع غير الضرورية
استعمل WeakMap عندما يكون مناسبًا
Debugging في Node.js
المشكلة في Node.js أحيانًا تشبه المتصفح، لكن السياق مختلف. لا DOM هنا، لكن هناك:
ملفات
مسارات
بيئة
عمليات async
Streams
قواعد بيانات
أخطاء تشغيل
مثال على قراءة ملف
const fs = require("fs");
fs.readFile("data.json", "utf8", (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(JSON.parse(data));
});
إذا فشل هنا، افحص:
هل الملف موجود؟
هل المسار صحيح؟
هل JSON صالح؟
هل الصلاحيات مناسبة؟
أخطاء شائعة في Node.js
المشكلة | السبب المحتمل | طريقة التصحيح |
|---|---|---|
| مسار أو package ناقص | تحقق من |
| المنفذ مستخدم | غيّر المنفذ أو أوقف العملية |
| صياغة خاطئة | راجع الملف |
| Promise بدون catch | أضف معالجة أخطاء |
أفضل طريقة لتصحيح الأخطاء في Promise chains
عندما تستخدم .then()، لا تنسَ أن تعيد القيم عند الحاجة.
مثال شائع على خطأ
fetch("/api/data")
.then((response) => {
response.json();
})
.then((data) => {
console.log(data);
});
هنا لم تُرجع response.json()، وبالتالي data قد تكون غير ما تتوقع.
الصحيح:
fetch("/api/data")
.then((response) => response.json())
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error(error);
});
Debugging مع الاختبارات
الاختبارات ليست فقط للتحقق من صحة الكود، بل أيضًا لتضييق مساحة الأخطاء.
لماذا الاختبارات مهمة في Debugging؟
تكشف الانكسار بسرعة
تعطيك مثالًا ثابتًا
تمنع إعادة ظهور الخطأ
تساعدك على فهم ما كان متوقعًا فعلًا
مثال Jest بسيط
function sum(a, b) {
return a + b;
}
test("adds two numbers", () => {
expect(sum(2, 3)).toBe(5);
});
إذا فشل الاختبار بعد تعديل ما، فهذا يعني أنك كسرت سلوكًا سابقًا.
استراتيجية عملية
ابدأ باختبار يفشل
أصلح السلوك
أعد تشغيل الاختبار
أضف حالات edge cases
لا تكتفِ بمسار النجاح فقط
Debugging حالات edge cases
أكثر الأخطاء إزعاجًا تحدث في الحالات النادرة:
مصفوفة فارغة
نص فارغ
قيمة
nullرقم صفر
بيانات مكررة
زمن وصول غير متوقع
تحديث متزامن من أكثر من مكان
مثال
function firstItem(items) {
return items[0];
}
هذا جيد حتى تأتي مصفوفة فارغة.
الأفضل:
function firstItem(items) {
if (!Array.isArray(items) || items.length === 0) {
return null;
}
return items[0];
}
جدول عملي: ماذا تفعل عندما يختفي الخطأ أو لا يظهر بوضوح؟
الحالة | الخطوة الأولى | الخطوة الثانية | الخطوة الثالثة |
|---|---|---|---|
الخطأ متقطع | سجّل كل المدخلات | أعد الإنتاج عدة مرات | اعزل السبب |
لا يوجد خطأ لكن النتيجة خاطئة | قارن المتوقع بالفعلي | افحص القيم خطوة خطوة | ضع breakpoint |
المشكلة في الواجهة | فحص DOM وCSS | افحص الأحداث | راقب state |
المشكلة في API | افحص network | راقب status | تحقق من body |
المشكلة في الأداء | استخدم Performance | راقب loops | قلّل عمليات DOM |
تقنية “divide and conquer”
عندما يصبح الكود كبيرًا، لا تحاول فهمه دفعة واحدة. قسّمه.
مثال
إذا كانت دالة كبيرة تفشل، اسأل:
هل المشكلة قبل جلب البيانات؟
أم أثناء المعالجة؟
أم عند العرض؟
أم بعد التفاعل مع المستخدم؟
ثم اختبر كل مرحلة وحدها.
function processProfile(profile) {
const normalized = normalizeProfile(profile);
const validated = validateProfile(normalized);
return formatProfile(validated);
}
بدلًا من تصحيح الدالة كلها مرة واحدة، اختبر:
console.log(normalizeProfile(profile));
console.log(validateProfile(normalized));
console.log(formatProfile(validated));
أخطاء صغيرة تصنع فوضى كبيرة
هذه بعض الأخطاء التي تكرر نفسها كثيرًا:
1) نسيان return
function getNumber() {
const n = 5;
}
2) المقارنة المربكة
if (value == false) {
// ...
}
الأفضل غالبًا:
if (value === false) {
// ...
}
أو حتى إعادة صياغة الشرط ليكون أوضح.
3) تغيير البيانات الأصلية دون قصد
const arr = [1, 2, 3];
const copy = arr;
copy.push(4);
الآن arr تغيرت أيضًا لأن copy تشير لنفس المرجع.
4) التعامل مع async كأنه sync
let result;
fetch("/api").then((r) => r.json()).then((data) => {
result = data;
});
console.log(result);
غالبًا سيطبع undefined لأن التنفيذ لم ينتظر.
كيف تبني روتين Debugging احترافي؟
بدل التصرف بعشوائية، اجعل عندك خطوات ثابتة:
الروتين المقترح
اقرأ الخطأ كاملًا.
حدّد أين حدث.
أعد إنتاجه.
جرّد المشكلة إلى مثال صغير.
راقب القيم.
استخدم breakpoint أو console.
صحح سبب المشكلة لا عرضها.
اختبر بعد الإصلاح.
أضف اختبارًا إن أمكن.
تأكد أن الحل لم يكسر شيئًا آخر.
مثال كامل على Debugging من البداية للنهاية
المشكلة
زر الحفظ لا يحدّث الحالة.
الكود
let saved = false;
document.querySelector("#save").addEventListener("click", async () => {
const response = fetch("/api/save");
if (response.ok) {
saved = true;
}
});
ماذا نلاحظ؟
fetchلم يُنتظرresponseهو Promise، وليس response فعليًالا يوجد
awaitresponse.okلن يعمل كما يجب
الإصلاح
let saved = false;
document.querySelector("#save").addEventListener("click", async () => {
try {
const response = await fetch("/api/save");
if (!response.ok) {
throw new Error(`Save failed: ${response.status}`);
}
saved = true;
console.log("Saved successfully");
} catch (error) {
console.error("Error saving:", error);
}
});
لماذا هذا أفضل؟
أوضح
أكثر أمانًا
يلتقط الأخطاء
يعالج status
يسجل السبب الحقيقي عند الفشل
جدول مرجعي سريع للمحترف
المهمة | أفضل أداة | لماذا |
|---|---|---|
معرفة القيمة الحالية |
| سريع وبسيط |
تتبع الاستدعاء |
| يظهر المسار |
إيقاف التنفيذ | breakpoint | أدق من الطباعة |
فحص الشبكة | Network tab | يكشف الطلبات |
فحص الأداء | Performance | يكشف البطء |
فحص الذاكرة | Memory tab | يكشف التسرب |
قراءة بنية البيانات |
| واضح ومنظم |
اختبار منطق معين | unit test | يمنع التكرار |
نصائح ذهبية تقلل الأخطاء من الأصل
أحيانًا أفضل Debugging هو الذي لا تحتاجه كثيرًا. وهذه بعض العادات التي تقلل الفوضى:
اكتب أسماء متغيرات واضحة
لا تكرر المنطق في أكثر من مكان
اجعل الدوال صغيرة
افصل العرض عن المنطق
تحقق من المدخلات مبكرًا
لا تفترض نوع البيانات
استخدم linting
اكتب اختبارات للحالات الحساسة
راقب الأخطاء في الـ console باستمرار
نظّف
console.logقبل النشر
متى تستخدم console.log ومتى لا تستخدمه؟
console.log مفيد، لكن ليس دائمًا أفضل اختيار.
استخدمه عندما:
تحتاج فحصًا سريعًا
تعمل على سلوك بسيط
تريد عرض قيمة مؤقتًا
لا تعتمد عليه وحده عندما:
المشكلة معقدة
هناك تداخل async
السلوك متكرر أو متشعب
تحتاج رؤية الـ call stack
تريد تحليل الأداء أو الذاكرة
مثال على Debugging أنيق مع Assertions
function multiplyPrice(price, quantity) {
console.assert(typeof price === "number", "price must be a number");
console.assert(typeof quantity === "number", "quantity must be a number");
return price * quantity;
}
هذا لا يوقف التطبيق في كل الحالات، لكنه ينبهك عندما يكون هناك شيء غير صحيح.
كيف تعرف أن المشكلة انتهت فعلًا؟
لا تكتفِ بأن السطر “اشتغل مرة”. تأكد من الآتي:
هل المشكلة تعالجت في كل الحالات؟
هل اختفت الرسائل الخطأ؟
هل لا يزال الأداء جيدًا؟
هل لم يتغير سلوك آخر؟
هل الاختبار يمر؟
هل الكود واضح لشخص آخر؟
هل عالجت السبب أم العرض؟
المحترف لا يفرح فقط لأن الخطأ اختفى، بل لأنه فهم لماذا حدث.
خاتمة
تصحيح أخطاء JavaScript ليس مجرد مهارة تقنية، بل أسلوب تفكير. كل مرة تواجه فيها bug، أنت أمام فرصة لتقوية عينك البرمجية: أن ترى ما لا يُرى مباشرة، وأن تربط السطر بالنتيجة، والسبب بالأثر، والتوقيت بالسلوك.
ابدأ دائمًا من الوصف الدقيق، ثم التكرار، ثم العزل، ثم الفحص، ثم الإصلاح. ومع الوقت ستلاحظ شيئًا جميلًا: لن تصبح فقط أسرع في حل المشاكل، بل ستصبح أفضل في كتابة كود أقل عرضة للمشاكل أصلًا.
الفرق الحقيقي ليس أن المحترف لا يخطئ. الفرق أنه يعرف أين ينظر، ومتى يتوقف عن التخمين، وكيف يحول الفوضى إلى خطوات واضحة.