كيفية استخدام Async/Await في JavaScript مع أمثلة عملية واضحة
مقدمة: لماذا نحتاج إلى Async/Await في JavaScript؟
عند بناء تطبيقات الويب الحديثة، نحتاج كثيراً إلى تنفيذ مهام تستغرق وقتاً مثل جلب البيانات من واجهة برمجية، أو قراءة ملف، أو الانتظار حتى تكتمل عملية معينة. هنا تظهر أهمية البرمجة غير المتزامنة في JavaScript، والتي تجعل التطبيق أكثر سلاسة من دون تجميد الواجهة أو تعطيل بقية الأوامر.
في هذا الدليل، سنتدرج لفهم الأساسيات التي تسبق Async/Await، وهي Event Loop وCallbacks وPromises، ثم ننتقل إلى طريقة الاستخدام الصحيحة مع أمثلة عملية.

ما هو Event Loop في JavaScript؟
تُعد آلية Event Loop من أهم المفاهيم لفهم طريقة عمل JavaScript. هذه اللغة تعمل بأسلوب single-threaded، أي إنها تنفذ مهمة واحدة فقط في اللحظة نفسها. ويتم ذلك عبر بنية أساسية تُعرف باسم Call Stack، حيث تدخل الأوامر للتنفيذ ثم تخرج بعد اكتمالها.
بمعنى أبسط: عندما تستدعي دالة مثل First()، تُدفع إلى Call Stack، وبعد انتهاء تنفيذها تُزال منه، ثم تنتقل المحرك إلى السطر التالي.

بعد انتهاء الدالة First() من التنفيذ، يتم تفريغها من Call Stack، ثم يتجه Event Loop إلى السطر التالي لوضعه في المكدس وتجهيزه للتنفيذ.

والنتيجة الظاهرة في وحدة التحكم ستكون كما يلي:

مثال يوضح التنفيذ غير المتزامن
console.log('First!');
setTimeout(function second() {
console.log('Timed Out!');
}, 0);
console.log('Final!');
عند تشغيل هذا المثال، ستُطبع القيمة First! أولاً، ثم ينتقل المحرك إلى setTimeout(). ورغم أن مدة الانتظار تساوي 0، فإن الدالة لا تُنفذ فوراً، بل تُرسل إلى قائمة المهام لتُنفذ في دورة لاحقة من Event Loop.
بعد ذلك يُنفذ السطر الأخير، فتظهر Final! قبل Timed Out!. وهذا يوضح أن بعض الدوال مثل setTimeout() لا تعمل مباشرة داخل Call Stack بالطريقة التقليدية.
تُعرف هذه المهام غالباً باسم Macro Tasks، ومن أمثلتها setTimeout() وsetInterval().

كيف تعمل Callbacks في JavaScript؟
دوال Callback هي ببساطة دوال تُمرر كوسيط إلى دالة أخرى ليتم استدعاؤها لاحقاً عند اكتمال مهمة محددة. كانت هذه الطريقة من أوائل أساليب التعامل مع العمليات غير المتزامنة في JavaScript.
مثال أساسي على عرض قائمة أفلام
const movies = [
{
title: `A New Hope`,
body: `After Princess Leia, the leader of the Rebel Alliance, is held hostage by Darth Vader, Luke and Han Solo must free her and destroy the powerful weapon created by the Galactic Empire.`
},
{
title: `The Empire Strikes Back`,
body: `Darth Vader is adamant about turning Luke Skywalker to the dark side. Master Yoda trains Luke to become a Jedi Knight while his friends try to fend off the Imperial fleet.`
}
];
function getMovies() {
setTimeout(() => {
movies.forEach((movie, index) => {
console.log(movie.title);
});
}, 1000);
}
getMovies();
في هذا المثال لدينا مصفوفة باسم movies، ودالة getMovies() مهمتها عرض عناوين الأفلام بعد تأخير بسيط.

المشكلة عند إضافة عنصر جديد بشكل غير متزامن
لنضف الآن دالة جديدة باسم createMovies() لإدراج فيلم إضافي:
const movies = [
{
title: `A New Hope`,
body: `After Princess Leia, the leader of the Rebel Alliance, is held hostage by Darth Vader, Luke and Han Solo must free her and destroy the powerful weapon created by the Galactic Empire.`
},
{
title: `The Empire Strikes Back`,
body: `Darth Vader is adamant about turning Luke Skywalker to the dark side. Master Yoda trains Luke to become a Jedi Knight while his friends try to fend off the Imperial fleet.`
}
];
function getMovies() {
setTimeout(() => {
movies.forEach((movie, index) => {
console.log(movie.title);
});
}, 1000);
}
function createMovies(movie) {
setTimeout(() => {
movies.push(movie);
}, 2000);
}
getMovies();
createMovies({
title: `Return of the Jedi`,
body: `Luke Skywalker attempts to bring his father back to the light side of the Force. At the same time, the rebels hatch a plan to destroy the second Death Star.`
});
في هذه الحالة، لن يظهر الفيلم الثالث في وحدة التحكم. السبب أن الدالة getMovies() تنتهي بعد ثانية واحدة، بينما تحتاج createMovies() إلى ثانيتين لإضافة الفيلم الجديد. لذلك يتم عرض القائمة قبل اكتمال الإضافة.

حل المشكلة باستخدام Callback
يمكننا تمرير دالة getMovies() كوسيط إلى createMovies()، ثم استدعاؤها فور اكتمال الإضافة:
const movies = [
{
title: `A New Hope`,
body: `After Princess Leia, the leader of the Rebel Alliance, is held hostage by Darth Vader, Luke and Han Solo must free her and destroy the powerful weapon created by the Galactic Empire.`
},
{
title: `The Empire Strikes Back`,
body: `Darth Vader is adamant about turning Luke Skywalker to the dark side. Master Yoda trains Luke to become a Jedi Knight while his friends try to fend off the Imperial fleet.`
}
];
function getMovies() {
setTimeout(() => {
movies.forEach((movie, index) => {
console.log(movie.title);
});
}, 1000);
}
function createMovies(movie, callback) {
setTimeout(() => {
movies.push(movie);
callback();
}, 2000);
}
createMovies(
{
title: `Return of the Jedi`,
body: `Luke Skywalker attempts to bring his father back to the light side of the Force. At the same time, the rebels hatch a plan to destroy the second Death Star.`
},
getMovies
);
بهذه الطريقة، يتم عرض القائمة بعد التأكد من إضافة الفيلم الجديد، فتظهر النتائج بالشكل المتوقع.

كيف تعمل Promises في JavaScript؟
تُعد Promise أسلوباً أحدث وأكثر تنظيماً للتعامل مع العمليات غير المتزامنة. وهي تمثل قيمة قد تصبح متاحة في المستقبل، أو قد تفشل العملية التي تنتجها.
للوعد البرمجي Promise ثلاث حالات أساسية:
- قيد الانتظار
pending: العملية لم تنتهِ بعد. - تم التنفيذ بنجاح
fulfilled: تمت العملية بنجاح. - تم الرفض
rejected: حدث خطأ أو فشلت العملية.
عند إنشاء Promise، نستقبل عادة الدالتين resolve وreject. إذا نجحت العملية نستخدم resolve()، وإذا فشلت نستخدم reject().
const movies = [
{
title: `A New Hope`,
body: `After Princess Leia, the leader of the Rebel Alliance, is held hostage by Darth Vader, Luke and Han Solo must free her and destroy the powerful weapon created by the Galactic Empire.`
},
{
title: `The Empire Strikes Back`,
body: `Darth Vader is adamant about turning Luke Skywalker to the dark side. Master Yoda trains Luke to become a Jedi Knight while his friends try to fend off the Imperial fleet.`
}
];
function getMovies() {
setTimeout(() => {
movies.forEach((movie, index) => {
console.log(movie.title);
});
}, 1000);
}
function createMovies(movie) {
return new Promise((resolve, reject) => {
setTimeout(() => {
movies.push(movie);
const error = false;
if (!error) {
resolve();
} else {
reject('Error: Something went wrong!');
}
}, 2000);
});
}
createMovies({
title: `Return of the Jedi`,
body: `Luke Skywalker attempts to bring his father back to the light side of the Force. At the same time, the rebels hatch a plan to destroy the second Death Star.`
}).then(getMovies);
بعد اكتمال Promise بنجاح، يتم استدعاء .then()، وهنا تُنفذ الدالة getMovies(). وإذا حدث خطأ، يمكن التعامل معه لاحقاً بواسطة .catch()، وهي ممارسة مهمة في التطبيقات الحقيقية.
لماذا تُعد Promises أفضل من Callbacks في كثير من الحالات؟
- تجعل تدفق الكود أوضح وأسهل قراءة.
- تقلل من مشكلة التداخل المفرط للدوال.
- تدعم سلاسل التنفيذ عبر
.then()و.catch(). - تُعد الأساس الذي بُنيت عليه
Async/Await.
ما هي Async/Await وكيف تعمل؟
تُستخدم Async/Await لكتابة كود غير متزامن بصياغة أقرب إلى الكود المتزامن العادي، وهذا يمنح المطور وضوحاً أكبر وسهولة أعلى في الصيانة.
عند كتابة الكلمة المفتاحية async قبل دالة، فإن هذه الدالة تُعيد دائماً Promise. أما الكلمة await فتُستخدم داخل دالة async لجعل التنفيذ ينتظر حتى يكتمل الوعد البرمجي.
async function example() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 2000);
});
let result = await promise;
alert(result);
}
example();
في هذا المثال، يتوقف تنفيذ الدالة مؤقتاً عند السطر الذي يحتوي على await إلى أن يتم حل Promise. بعد مرور ثانيتين، تصبح قيمة result هي done!، ثم تُعرض للمستخدم.
الفكرة الجوهرية وراء await
await لا توقف التطبيق كله، لكنها توقف فقط تنفيذ الدالة الحالية حتى تكتمل العملية غير المتزامنة. وهذا فرق مهم جداً، لأنه يسمح لبقية التطبيق بالاستمرار في العمل بشكل طبيعي.
مثال عملي على Async/Await مع قائمة الأفلام
لنحوّل المثال السابق من Promise إلى Async/Await:
const movies = [
{
title: `A New Hope`,
body: `After Princess Leia, the leader of the Rebel Alliance, is held hostage by Darth Vader, Luke and Han Solo must free her and destroy the powerful weapon created by the Galactic Empire.`
},
{
title: `The Empire Strikes Back`,
body: `Darth Vader is adamant about turning Luke Skywalker to the dark side. Master Yoda trains Luke to become a Jedi Knight while his friends try to fend off the Imperial fleet.`
}
];
function getMovies() {
setTimeout(() => {
movies.forEach((movie, index) => {
console.log(movie.title);
});
}, 1000);
}
function createMovies(movie) {
return new Promise((resolve, reject) => {
setTimeout(() => {
movies.push(movie);
const error = false;
if (!error) {
resolve();
} else {
reject('Error: Something went wrong!');
}
}, 2000);
});
}
async function init() {
await createMovies({
title: `Return of the Jedi`,
body: `Luke Skywalker attempts to bring his father back to the light side of the Force. At the same time, the rebels hatch a plan to destroy the second Death Star.`
});
getMovies();
}
init();
في هذا السيناريو، تنتظر الدالة init() اكتمال createMovies() أولاً، ثم تستدعي getMovies(). النتيجة هي عرض القائمة بعد تحديثها، من دون الحاجة إلى تمرير callback أو استخدام سلسلة طويلة من .then().
متى تستخدم Async/Await؟
تكون Async/Await مناسبة جداً عندما تحتاج إلى:
- تنفيذ طلبات
APIبالتسلسل. - قراءة بيانات أو حفظها قبل الانتقال إلى خطوة تالية.
- كتابة كود أوضح وأسهل في المراجعة.
- تقليل التعقيد الناتج عن تداخل
Callbacksأو تعددPromises.
أفضل الممارسات عند الاستخدام
- استخدم
try...catchلمعالجة الأخطاء. - لا تفرط في استخدام
awaitداخل حلقات إذا كانت المهام يمكن تنفيذها بالتوازي. - تأكد من أن الدالة المعنية تُعيد
Promiseقبل استخدامawait. - اجعل أسماء الدوال معبرة، مثل
fetchUsers()أوsaveOrder().
مثال مختصر لمعالجة الخطأ
async function init() {
try {
await createMovies({
title: `Return of the Jedi`,
body: `Luke Skywalker attempts to bring his father back to the light side of the Force. At the same time, the rebels hatch a plan to destroy the second Death Star.`
});
getMovies();
} catch (error) {
console.log(error);
}
}
هذا الأسلوب يجعل التعامل مع الأخطاء أكثر احترافية، خصوصاً في التطبيقات التي تعتمد على الشبكات أو البيانات الخارجية.
مقارنة سريعة بين Callbacks وPromises وAsync/Await
| الأسلوب | سهولة القراءة | إدارة الأخطاء | الملاءمة للمشاريع الحديثة |
|---|---|---|---|
Callbacks |
متوسطة إلى ضعيفة عند التوسع | أقل مرونة | مناسبة للحالات البسيطة |
Promises |
جيدة | أفضل عبر .catch() |
شائعة جداً |
Async/Await |
ممتازة | مريحة مع try...catch |
الأفضل في أغلب التطبيقات الحديثة |
الخلاصة التقنية
إذا أردت فهم Async/Await بشكل صحيح، فلا يكفي حفظ الصياغة فقط، بل يجب أولاً استيعاب آلية Event Loop وفكرة Callbacks ثم Promises. عملياً، تُعد Async/Await أفضل واجهة لكتابة الكود غير المتزامن في JavaScript لأنها تجعل المنطق أوضح، وتقلل الأخطاء، وتسهل صيانة المشروع مع نموه. ومع ذلك، فهي في النهاية مبنية على Promises، لذا يبقى فهم الأساس ضرورياً لأي مطور يريد كتابة كود احترافي وموثوق.