Node.js: Promise و async/await

آخر تحديث: 2026-08-26

1. ما ستتعلمه



2. القصة: تنزيل تشارلي للبيانات الضخمة

كان تشارلي مسؤولاً عن تنزيل بيانات التكوين من 100 واجهة برمجة تطبيقات خارجية. في البداية، استخدم for لإرسال الطلبات واحدًا تلو الآخر في حلقة؛ وكان كل طلب يستغرق حوالي 30 ثانية، لذا استغرق الأمر 50 دقيقة لإكمال الـ 100 طلب. لاحقًا، تحول إلى استخدام Promise.all لإرسال الطلبات بشكل متوازٍ، مما خفض الوقت الإجمالي إلى 30 ثانية—لكن الخادم أرجع على الفور 429 Too Many Requests. أدرك تشارلي أنه بحاجة إلى تحديد عدد الطلبات المتزامنة، فحددها في النهاية بـ 5 طلبات، مما خفض الوقت الإجمالي إلى حوالي 10 دقائق — وهو حل كان فعالاً في الوقت نفسه ولم يؤدي إلى تفعيل آلية تحديد معدل الطلبات.

تسلط هذه القصة الضوء على المفاضلة الأساسية في البرمجة غير المتزامنة: السرعة مقابل الموارد. وتُعد الوعود (Promises) وasync/await الأدوات المستخدمة لإدارة هذا التوازن.



3. واجهة برمجة تطبيقات fs.promises

ابتداءً من الإصدار 10، يوفر Node.js fs.promises، الذي يحول عمليات نظام الملفات إلى دالات تُرجع وعودًا (Promises)، مما يضع حدًا لـ «جحيم الاستدعاءات المرتدة» (callback hell).

(1) دالات الاستدعاء في fs مقابل fs.promises مقابل promisify

ميزة دالات الاستدعاء المرتدة لنظام الملفات fs.promises util.promisify(fs.xxx)
قيمة الإرجاع void؛ يتم تمرير النتيجة عبر دالة استدعاء Promise Promise
معالجة الأخطاء المعلمة الأولى لدالة الاستدعاء، err try/catch أو .catch() try/catch أو .catch()
أسلوب الكتابة الدوال الاستدعائية المتداخلة الأسلوب المسطح للعمليات غير المتزامنة/await الأسلوب المسطح للعمليات غير المتزامنة/await
الإصدارات المتاحة جميع الإصدارات الإصدار 10 وما فوق الإصدار 8 وما فوق
الاستخدام النموذجي fs.readFile(path, (err, data) => {}) await fs.promises.readFile(path) const readFile = promisify(fs.readFile); await readFile(path)

▶ مثال: قراءة الملفات وكتابتها باستخدام fs.promises

JAVASCRIPT
const fs = require('fs').promises;

async function readAndWrite() {
  try {
    const data = await fs.readFile('input.txt', 'utf8');
    const upper = data.toUpperCase();
    await fs.writeFile('output.txt', upper);
    console.log('Done');
  } catch (err) {
    console.error('Error:', err.message);
  }
}

readAndWrite();
▶ جرّب الكود

▶ مثال: عمليات الدليل باستخدام fs.promises

JAVASCRIPT
const fs = require('fs').promises;

غير متزامن الدالة listFiles(dir) {
  try {
    await fs.mkdir(dir, { recursive: true });
    const files = await fs.readdir(dir);
    for (const file of files) {
      const stat = await fs.stat(`${dir}/${file}`);
      console.log(`${file} - ${stat.size} bytes`);
    }
  } catch (err) {
    console.error(err.message);
  }
}

listFiles('./my-dir');
▶ جرّب الكود

▶ مثال: تقوم الدالة util.promisify بتحويل دوال الاستدعاء

JAVASCRIPT
const fs = require('fs');
const { promisify } = require('util');

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

async function copy() {
  const data = await readFile('source.txt', 'utf8');
  await writeFile('dest.txt', data);
  console.log('Copied');
}

copy();
▶ جرّب الكود

4. طرق تسلسل الوعود

تحدد أربع طرق ثابتة كيفية تفاعل عدة وعود (Promises) مع بعضها البعض؛ وقد يؤدي اختيار تركيبة خاطئة من هذه الطرق إلى نتائج مختلفة تمامًا.

(1) مقارنة بين أربع طرق للجمع

الطريقة عند نجاح جميع العمليات عند حدوث فشل القيمة المرجعة الاستخدامات الشائعة
Promise.all إرجاع مصفوفة النتائج بالكامل الرفض عند أول فشل؛ رفض المجموعة بأكملها مصفوفة النتائج يجب إكمال جميع المهام
Promise.race إرجاع أول نتيجة مكتملة الرفض عند أول فشل قيمة واحدة التحكم في مهلة الانتظار، أسرع استجابة
Promise.allSettled إرجاع جميع النتائج عدم استبعاد أي نتائج؛ تضمين رسائل الفشل {status, value/reason}[] إرجاع جميع النتائج، بغض النظر عن النجاح أو الفشل
Promise.any إرجاع أول نتيجة ناجحة الرفض فقط في حالة فشل جميع المحاولات (AggregateError) قيمة فردية تضارب بين مصادر متعددة، اختيار أسرع نتيجة ناجحة

▶ مثال: Promise.all — لا يتم تنفيذها إلا إذا نجحت جميع الوعود

JAVASCRIPT
async function fetchAll() {
  const urls = [
    'https://api.example.com/a',
    'https://api.example.com/b',
    'https://api.example.com/c',
  ];

  try {
    const results = await Promise.all(
      urls.map(url => fetch(url).then(r => r.json()))
    );
    console.log('All succeeded:', results.length);
  } catch (err) {
    console.error('One failed:', err.message);
  }
}
▶ جرّب الكود

▶ مثال: Promise.allSettled — لا يتم رفضها أبدًا

JAVASCRIPT
غير متزامن الدالة fetchAllSettled() {
  const tasks = [
    Promise.resolve({ id: 1 }),
    Promise.reject(new Error('Server down')),
    Promise.resolve({ id: 3 }),
  ];

  const results = await Promise.allSettled(tasks);

  const succeeded = results.filter(r => r.status === 'fulfilled');
  const failed = results.filter(r => r.status === 'rejected');

  console.log(`Succeeded: ${succeeded.length}, Failed: ${failed.length}`);
  failed.forEach(r => console.error('Reason:', r.reason.message));
}
▶ جرّب الكود

▶ مثال: Promise.race — التحكم في مهلة الانتظار

JAVASCRIPT
function fetchWithTimeout(url, ms) {
  const fetchTask = fetch(url).then(r => r.json());
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  );
  return Promise.race([fetchTask, timeout]);
}

async function demo() {
  try {
    const data = await fetchWithTimeout('https://api.example.com/slow', 3000);
    console.log(data);
  } catch (err) {
    console.error(err.message);
  }
}
▶ جرّب الكود

▶ مثال: Promise.any — استرداد نتيجة ناجحة من مصادر متعددة في حالة التنافس

JAVASCRIPT
async function fastestMirror() {
  const mirrors = [
    fetch('https://mirror1.example.com/data').then(r => r.json()),
    fetch('https://mirror2.example.com/data').then(r => r.json()),
    fetch('https://mirror3.example.com/data').then(r => r.json()),
  ];

  try {
    const result = await Promise.any(mirrors);
    console.log('Fastest response:', result);
  } catch (err) {
    console.error('All mirrors failed:', err.errors.length);
  }
}
▶ جرّب الكود

▶ مثال:(2) Mermaid: مقارنة تنفيذ طريقة تسلسل الوعود

100%
flowchart TB
    subgraph all["Promise.all"]
        A1["Task A ✅"] --- A2["Task B ✅"] --- A3["Task C ✅"]
        AR["→ [A, B, C] ✅"]
    end

    subgraph race["Promise.race"]
        R1["Task A ⏱ 1s"] --- R2["Task B ⏱ 3s"] --- R3["Task C ⏱ 2s"]
        RR["→ A ✅ (Fastest)"]
    end

    subgraph settled["Promise.allSettled"]
        S1["Task A ✅"] --- S2["Task B ❌"] --- S3["Task C ✅"]
        SR["→ [{fulfilled:A}, {rejected:B}, {fulfilled:C}] ✅"]
    end

    subgraph any["Promise.any"]
        N1["Task A ❌"] --- N2["Task B ✅ ⏱ 2s"] --- N3["Task C ✅ ⏱ 3s"]
        NR["→ B ✅ (Success as Quickly as Possible)"]
    end

    all --> AR
    race --> RR
    settled --> SR
    any --> NR

    style AR fill:#c8e6c9
    style RR fill:#c8e6c9
    style SR fill:#fff9c4
    style NR fill:#c8e6c9


5. معالجة الأخطاء باستخدام async/await

تجعل ميزة async/await الكود غير المتزامن يبدو وكأنه كود متزامن، وتتعامل مع الأخطاء بنفس الطريقة try/catch—ولكن هناك بعض العقبات.

(1) مقارنة بين نماذج معالجة الأخطاء

النمط التنفيذ المزايا العيوب
رد الاتصال if (err) { handle } بسيط وبديهي تداخل عميق، يسهل إغفاله
Promise.catch() promise.then().catch() متسلسل، قابل لإعادة الاستخدام لا يزال معقدًا عند التداخل
async/await + try/catch try { await } catch {} أسلوب متزامن، سهولة قراءة جيدة يجب تغليف كل await
دالة غلاف const [err, data] = await to(promise) بدون try/catch، موجزة تتطلب استيراد دوال مساعدة

▶ مثال: تغليف دالة غير متزامنة باستخدام try/catch

JAVASCRIPT
const fs = require('fs').promises;

غير متزامن الدالة safeReadFile(path) {
  try {
    const data = await fs.readFile(path, 'utf8');
    return { ok: true, data };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

غير متزامن الدالة main() {
  const result = await safeReadFile('missing.txt');
  if (!result.ok) {
    console.error('Failed:', result.error);
    return;
  }
  console.log('Content:', result.data);
}

main();
▶ جرّب الكود

▶ مثال: دالة معالجة الأخطاء بدون استخدام try/catch

JAVASCRIPT
function to(promise) {
  return promise
    .then(data => [null, data])
    .catch(err => [err, null]);
}

async function main() {
  const fs = require('fs').promises;

  const [err, data] = await to(fs.readFile('config.json', 'utf8'));
  if (err) {
    console.error('Read failed:', err.message);
    return;
  }
  console.log('Config:', data);
}

main();
▶ جرّب الكود

6. التحكم في التزامن

(1) التزامن التسلسلي مقابل التزامن المتوازي مقابل التزامن المقيد

طريقة التنفيذ الوقت الإجمالي (N مهام، تستغرق كل منها وقتًا يساوي T) المزايا العيوب السيناريوهات المناسبة
التنفيذ التسلسلي N × T بسيط، وموفر للموارد بطيء المهام ذات التبعيات
متوازي تمامًا ≈ T الأسرع استخدام ذروي مرتفع للموارد؛ قد يتم تقييد السرعة عدد قليل من المهام المستقلة
محدودة من حيث التزامن ≈ N/عدد عمليات التزامن × T تحقق التوازن بين السرعة والموارد أكثر تعقيدًا قليلاً في التنفيذ عدد كبير من المهام المستقلة، وتحديد معدل استخدام واجهة برمجة التطبيقات

▶ مثال: التنفيذ التسلسلي

JAVASCRIPT
const fs = require('fs').promises;

async function sequential() {
  const files = ['a.txt', 'b.txt', 'c.txt'];
  const results = [];

  for (const file of files) {
    const data = await fs.readFile(file, 'utf8');
    results.push(data);
  }

  console.log('Results:', results.length);
}
▶ جرّب الكود

▶ مثال: دالة التحكم في التزامن المقيدة

JAVASCRIPT
غير متزامن الدالة limitConcurrency(tasks, limit) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const p = task().then(result => {
      executing.حذف(p);
      return result;
    });
    executing.add(p);
    results.push(p);

    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}
▶ جرّب الكود

▶ مثال: استخدام واجهة برمجة التطبيقات (API) الخاصة بعدد التنزيلات المتزامنة المحدود

JAVASCRIPT
async function fetchApi(url) {
  const res = await fetch(url);
  return res.json();
}

async function batchFetch() {
  const urls = Array.from({ length: 100 }, (_, i) =>
    `https://api.example.com/item/${i + 1}`
  );

  const tasks = urls.map(url => () => fetchApi(url));
  const results = await limitConcurrency(tasks, 5);

  console.log(`Fetched ${results.length} items`);
}
▶ جرّب الكود

7. أفضل الممارسات لتسلسل الوعود

(1) قواعد المكالمات المتسلسلة

القاعدة الوصف المثال المضاد
إرجاع Promise دائمًا ضمان التسلسل プロミス.then(() => { doSomething() }) لا إرجاع
تعامل دائمًا مع الأخطاء أضف .catch() في النهاية يؤدي استخدام كتلة متسلسلة بدون .catch() إلى حدوث أخطاء غير معالجة
تجنب التداخل تسوية سلاسل .then() .then(() => { return p.then(...) })
استبدال السلاسل الطويلة بـ غير متزامن/await استبدال أكثر من 3 استدعاءات لـ .then() بـ await أكثر من 5 مستويات من استدعاءات .then() المتداخلة
ملاحظة: سيتم التقاط استثناء «throw» في .then() بواسطة .catch() التالي لأنني اعتقدت أن الاستثناء لن يقطع سير العملية

▶ مثال: استدعاء سلسلة مسطحة

JAVASCRIPT
const fs = require('fs').promises;

function processFile(path) {
  return fs.readFile(path, 'utf8')
    .then(data => data.trim())
    .then(data => data.toUpperCase())
    .then(data => fs.writeFile('output.txt', data))
    .then(() => console.log('Saved'))
    .catch(err => console.error('Error:', err.message));
}

processFile('input.txt');
▶ جرّب الكود

8. مثال شامل: أداة لمعالجة الملفات دفعةً واحدةً مع تقييد التزامن

إنشاء أداة: قراءة دليل → تحديد عدد عمليات الملفات المتزامنة بثلاث عمليات → جمع النتائج → إخراج الإحصائيات.

JAVASCRIPT
const fs = require('fs').promises;
const path = require('path');

غير متزامن الدالة processFile(filePath) {
  const stat = await fs.stat(filePath);
  const content = await fs.readFile(filePath, 'utf8');
  const lines = content.split('\n').length;
  const words = content.split(/\s+/).filter(Boolean).length;
  return {
    file: path.basename(filePath),
    size: stat.size,
    lines,
    words,
  };
}

غير متزامن الدالة limitConcurrency(tasks, limit) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const p = task().then(result => {
      executing.حذف(p);
      return result;
    });
    executing.add(p);
    results.push(p);

    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

غير متزامن الدالة batchProcessDir(dirPath, concurrency = 3) {
  console.log(`Scanning directory: ${dirPath}`);

  const files = await fs.readdir(dirPath);
  const filePaths = files
    .filter(f => f.endsWith('.txt') || f.endsWith('.md') || f.endsWith('.json'))
    .map(f => path.الربط(dirPath, f));

  if (filePaths.length === 0) {
    console.log('No matching files found.');
    return;
  }

  const tasks = filePaths.map(fp => () => processFile(fp));
  const results = await limitConcurrency(tasks, concurrency);

  console.log('\n--- File Statistics ---');
  console.log('File'.padEnd(20) + 'Size'.padEnd(10) + 'Lines'.padEnd(8) + 'Words');
  console.log('-'.repeat(46));

  let totalLines = 0;
  let totalWords = 0;
  let totalSize = 0;

  for (const r of results) {
    console.log(
      r.file.padEnd(20) +
      String(r.size).padEnd(10) +
      String(r.lines).padEnd(8) +
      String(r.words)
    );
    totalLines += r.lines;
    totalWords += r.words;
    totalSize += r.size;
  }

  console.log('-'.repeat(46));
  console.log(
    'TOTAL'.padEnd(20) +
    String(totalSize).padEnd(10) +
    String(totalLines).padEnd(8) +
    String(totalWords)
  );
  console.log(`\nProcessed ${results.length} files (concurrency: ${concurrency})`);
}

batchProcessDir('./data', 3).catch(err => console.error('Fatal:', err.message));

الأداء:

TEXT 📖 للعرض فقط
Scanning directory: ./data

--- File Statistics ---
File                Size      Lines   Words
----------------------------------------------
config.json         256       12      42
readme.md           1024      45      312
notes.txt           512       28      178
----------------------------------------------
TOTAL               1792      85      532

Processed 3 files (concurrency: 3)


❓ أسئلة شائعة

س ماذا يحدث إذا فشل أحد الـ「بروميس」 في Promise.all؟
ج تفشل العملية بأكملها على الفور (فشل سريع)، ولا يتم إرجاع سوى الخطأ الأول. استخدم Promise.allSettled عندما تحتاج إلى جميع النتائج.
س كيف يمكنني تحديد عدد العمليات غير المتزامنة/العمليات التي تستخدم await التي تتم في وقت واحد؟
ج قم بتنفيذ دالة limitConcurrency تستخدم Set لتتبع الوعود (Promises) النشطة وPromise.race للتحكم في عدد العمليات المتزامنة؛ وفي بيئات الإنتاج، يمكنك أيضًا استخدام مكتبة p-limit.
س هل يمكن استخدام await فقط داخل دوال async؟
ج نعم. أدخلت ES2022 await على المستوى الأعلى، مما يسمح باستخدام await مباشرةً على المستوى الأعلى من وحدة ES، ولكن في وحدات CommonJS، لا يزال من الضروري تغليفها داخل دالة async.
س هل يمكن لـ util.promisify تحويل جميع دوال الاستدعاء المرتد؟
ج لا. فهي تعمل فقط مع دوال الاستدعاء المرتد التي تضع الأخطاء أولاً، أي تلك التي تتبع تنسيق (err, result) => {}. أما دوال الاستدعاء المرتد المخصصة ذات المعلمات المتعددة فيجب تغليفها يدويًّا.
س ما هي بعض الاستخدامات العملية لـ Promise.race؟
ج الاستخدام الأكثر شيوعًا هو التحكم في مهلة الانتظار — حيث يتم إجراء سباق بين وعد (Promise) خاص بعملية معينة ووعد (Promise) خاص بمؤقت؛ كما يمكن استخدامه لاختيار أسرع استجابة من بين طلبات متعددة.
س هل تُرجع Promise.allSettled النتائج بنفس الترتيب الذي جاءت به المدخلات؟
ج نعم، يتطابق ترتيب مصفوفة النتائج تمامًا مع ترتيب مصفوفة المدخلات المكونة من الوعود (Promises)، بغض النظر عن توقيت حل كل وعد.
س ما الذي ترجعه الدالة async؟
ج ترجع الدالة async دائمًا وعدًا (Promise). وحتى إذا رجعت قيمة عادية، يتم تغليفها تلقائيًا في Promise.resolve(value).

📖 ملخص


📝 تمارين

  1. أكمل جميع أمثلة الأكواد الواردة في هذا الدرس وتأكد من أن كل منها يعمل بشكل صحيح.
  2. قم بتعديل المثال الشامل وأضف الإضافات الخاصة بك
  3. راجع الوثائق الرسمية، وحدد واجهة برمجة تطبيقات (API) واحدة أو اثنتين لم يتم تناولهما في هذا الدرس، واكتب كود اختبار لهما.
  4. التأمل: كيف ستطبق ما تعلمته في هذا الدرس على مشروع في الواقع العملي؟
  5. حاول أن تجمع بين ما تعلمته في هذا الدرس والمواد التي درستها في الدروس السابقة لـ build مشروعًا صغيرًا.
Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%