تخطَّ إلى المحتوى
NerdLevelTech
تطوير الواجهة الأمامية

Async JavaScript: Callbacks و Promises و Async/Await

افهم الـ callbacks، والـ Promises، والـ async/await، والـ event loop، والـ AbortController مع أمثلة قابلة للتنفيذ وأنماط لمعالجة الأخطاء.

Nerd Level Techنُشر في تم التحديث ١٠ دقائق للقراءةEnglish
صورة غلاف: Async JavaScript: Callbacks و Promises و Async/Await

أعيد إنشاؤه: ٢٧ مارس ٢٠٢٦ · آخر تحقق: ٣ نوفمبر ٢٠٢٦.

ملخص

تطورت الـ asynchrony في JavaScript من الـ callbacks (التي أدت إلى تداخل "هرم الجحيم" pyramid-of-doom) إلى الـ Promises (القابلة للتسلسل، مع معالجة أفضل للأخطاء) وصولاً إلى async/await (كود يبدو متزامناً مع قوة الـ async الكاملة)—حيث يبني كل تجريد على ما سبقه، لمعالجة القيود السابقة.

البرمجة غير المتزامنة (Asynchronous programming) هي أساس في JavaScript. سواء كنت تقوم بجلب بيانات، أو قراءة ملفات، أو التعامل مع تفاعلات المستخدم، فإن العمليات غير المتزامنة موجودة في كل مكان. لكن الـ async لم تكن دائماً بهذه الأناقة—فقد عانى المطورون سابقاً من "جحيم الـ callback"، و"هرم الجحيم"، ومعالجة الأخطاء المتشابكة. يتتبع هذا الدليل التطور من الـ callbacks إلى الـ Promises ثم إلى async/await، موضحاً كيف حل كل ابتكار المشكلات السابقة مع تقديم أنماط جديدة. بنهاية هذا الدليل، لن تفهم فقط كيفية استخدام async/await، بل ستعرف لماذا هي الخيار الافتراضي في عام ٢٠٢٦.

الـ Callbacks: النمط الأصلي

الـ Callbacks هي دوال يتم تمريرها كـ arguments ليتم تنفيذها بعد اكتمال عملية معينة.

نمط الـ Callback الأساسي

javascript
// Callback function (synchronous use)
const processData = (data, callback) => {
  callback(data.toUpperCase());
};

processData('hello', result => {
  console.log(result); // 'HELLO'
});

// Asynchronous callback
const fetchData = (url, callback) => {
  setTimeout(() => {
    callback({ id: 1, name: 'Alice' });
  }, 100);
};

fetchData('/api/user', user => {
  console.log('User:', user.name);
});

جحيم الـ Callback (هرم الجحيم)

تصبح الـ Callbacks إشكالية عندما تتداخل العمليات—حيث يزداد مستوى الإزاحة (indentation) في كل مستوى، مما يخلق كوداً يصعب قراءته.

javascript
// Callback hell: deeply nested callbacks
getUser(userId, user => {
  getPosts(user.id, posts => {
    getComments(posts[0].id, comments => {
      getAuthor(comments[0].authorId, author => {
        console.log('Final author:', author.name);
        // We're 4 levels deep!
      });
    });
  });
});

// Problems:
// 1. Hard to read and maintain
// 2. Error handling is awkward (need try-catch at each level)
// 3. Code flows right, not down
// 4. Difficult to parallelize operations

معالجة الأخطاء باستخدام الـ Callbacks

تفتقر الـ Callbacks إلى معالجة مدمجة للأخطاء—يجب عليك تمرير كائنات الخطأ يدوياً أو استخدام callbacks منفصلة للأخطاء.

javascript
// Convention: error-first callbacks
const readFile = (filename, callback) => {
  setTimeout(() => {
    if (filename === 'missing.txt') {
      callback(new Error('File not found'), null);
    } else {
      callback(null, 'File contents');
    }
  }, 100);
};

readFile('data.txt', (error, data) => {
  if (error) {
    console.error('Error:', error.message);
  } else {
    console.log('Data:', data);
  }
});

// Nested error handling (tedious)
readFile('file1.txt', (err1, data1) => {
  if (err1) {
    console.error(err1);
    return;
  }
  readFile('file2.txt', (err2, data2) => {
    if (err2) {
      console.error(err2);
      return;
    }
    console.log('Both files:', data1, data2);
  });
});

الـ Promises: Async قابلة للتسلسل

تمثل الـ Promises قيمة مستقبلية وتوفر .then()، و .catch()، و .finally() لتركيب كود أكثر نظافة.

إنشاء واستخدام الـ Promises

javascript
// Creating a Promise
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Success!');
    // or: reject(new Error('Failed!'));
  }, 100);
});

// Consuming a Promise
promise
  .then(result => {
    console.log(result); // 'Success!'
    return result + ' More work';
  })
  .then(result => {
    console.log(result); // 'Success! More work'
  })
  .catch(error => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log('Operation complete');
  });

التسلسل (Chaining) مقابل التداخل (Nesting)

تقضي الـ Promises على التداخل من خلال السماح بالتسلسل.

javascript
// Without Promises (callback hell)
getUser(userId, user => {
  getPosts(user.id, posts => {
    getComments(posts[0].id, comments => {
      console.log('Comments:', comments);
    });
  });
});

// With Promises (clean chain)
getUser(userId)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => {
    console.log('Comments:', comments);
  })
  .catch(error => {
    console.error('Error:', error.message);
  });

// Promise-returning helpers
const getUser = (id) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id, name: 'Alice' });
    }, 100);
  });
};

const getPosts = (userId) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([{ id: 1, title: 'Post 1' }]);
    }, 100);
  });
};

انتشار الخطأ (Error Propagation)

في سلاسل الـ Promise، تنتقل الأخطاء إلى أول معالج .catch().

javascript
Promise.resolve()
  .then(() => {
    throw new Error('Step 1 failed');
  })
  .then(() => {
    console.log('Step 2 (skipped)');
  })
  .then(() => {
    console.log('Step 3 (skipped)');
  })
  .catch(error => {
    console.error('Caught error:', error.message); // 'Step 1 failed'
  });

// Error recovery
Promise.reject('Network error')
  .catch(error => {
    console.log('Retrying...');
    return 'recovered'; // New Promise with value 'recovered'
  })
  .then(result => {
    console.log('Recovered:', result); // 'Recovered: recovered'
  });

العمليات المتوازية باستخدام Promise.all()

javascript
// Fetch multiple resources in parallel
const userId = 1;

Promise.all([
  fetch(`/api/users/${userId}`).then(r => r.json()),
  fetch(`/api/posts/${userId}`).then(r => r.json()),
  fetch(`/api/comments/${userId}`).then(r => r.json())
])
  .then(([user, posts, comments]) => {
    console.log('All data:', { user, posts, comments });
  })
  .catch(error => {
    console.error('One of the requests failed:', error);
  });

// Promise.race: return first completed Promise
Promise.race([
  fetch('/api/data'),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 5000)
  )
])
  .then(response => console.log('Response received'))
  .catch(error => console.error('Request timeout or failed'));

Async/Await: كود Async يبدو متزامناً

تعتبر async/await مجرد "تجميل برمجي" (syntactic sugar) فوق الـ Promises، تتيح لك كتابة كود async يُقرأ وكأنه كود متزامن (synchronous).

أساسيات async/await

javascript
// Function declaration
async function fetchUser(id) {
  // Inside async function, you can use 'await'
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user; // Returns a Promise
}

// Arrow function
const fetchPost = async (id) => {
  const response = await fetch(`/api/posts/${id}`);
  return response.json();
};

// Usage (inside an async function)
async function main() {
  const user = await fetchUser(1);
  console.log('User:', user);
}
main();

// Or, in an ES module (ES2022+), you can use top-level await directly:
// const user = await fetchUser(1);
//
// Note: outside ES modules, await can ONLY be used inside async functions.

معالجة الأخطاء باستخدام try/catch

javascript
async function fetchUserData(id) {
  try {
    const response = await fetch(`/api/users/${id}`);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Failed to fetch user:', error.message);
    throw error; // Re-throw or return fallback
  } finally {
    console.log('Fetch attempt completed');
  }
}

// Usage (inside an async function or ES2022+ module top level)
async function run() {
  try {
    const user = await fetchUserData(1);
    console.log('User:', user);
  } catch (error) {
    console.error('Error in app:', error);
  }
}
run();

العمليات المتوازية باستخدام async/await

javascript
// Sequential: operations run one after another
async function fetchSequential(userId) {
  const user = await fetch(`/api/users/${userId}`).then(r => r.json());
  const posts = await fetch(`/api/posts/${userId}`).then(r => r.json());
  const comments = await fetch(`/api/comments/${userId}`).then(r => r.json());
  // Total time: sum of all requests
  return { user, posts, comments };
}

// Parallel: operations start simultaneously
async function fetchParallel(userId) {
  // Start all requests at once
  const userPromise = fetch(`/api/users/${userId}`).then(r => r.json());
  const postsPromise = fetch(`/api/posts/${userId}`).then(r => r.json());
  const commentsPromise = fetch(`/api/comments/${userId}`).then(r => r.json());

  // Wait for all to complete
  const [user, posts, comments] = await Promise.all([
    userPromise,
    postsPromise,
    commentsPromise
  ]);

  // Total time: max of all requests (much faster!)
  return { user, posts, comments };
}

// Using Promise.all with await
async function fetchWithAll(userId) {
  const [user, posts, comments] = await Promise.all([
    fetch(`/api/users/${userId}`).then(r => r.json()),
    fetch(`/api/posts/${userId}`).then(r => r.json()),
    fetch(`/api/comments/${userId}`).then(r => r.json())
  ]);

  return { user, posts, comments };
}

إعادة المحاولة باستخدام التراجع الأسي (Exponential Backoff)

javascript
async function retryFetch(url, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json();
    } catch (error) {
      if (attempt === maxRetries) {
        throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
      }

      // Exponential backoff: 100ms, 200ms, 400ms, etc.
      const delay = 100 * Math.pow(2, attempt - 1);
      console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);

      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Usage (inside an async function or ES2022+ module top level)
(async () => {
  const data = await retryFetch('/api/unreliable-endpoint');
  console.log('Data:', data);
})();

AbortController: عمليات Async قابلة للإلغاء

يتيح لك AbortController إلغاء طلبات fetch والعمليات غير المتزامنة الأخرى.

javascript
// Create a controller
const controller = new AbortController();

// Pass its signal to fetch
const fetchPromise = fetch('/api/data', {
  signal: controller.signal
});

// Cancel after 5 seconds
const timeoutId = setTimeout(() => {
  controller.abort();
}, 5000);

// Handle cancellation
try {
  const response = await fetchPromise;
  const data = await response.json();
  console.log('Data:', data);
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Request was cancelled');
  } else {
    console.error('Network error:', error);
  }
} finally {
  clearTimeout(timeoutId);
}

// Real-world: abort on user action
const controller2 = new AbortController();

const downloadButton = document.getElementById('download');
const cancelButton = document.getElementById('cancel');

downloadButton.addEventListener('click', async () => {
  try {
    const response = await fetch('/api/large-file', {
      signal: controller2.signal
    });
    const blob = await response.blob();
    console.log('Download complete');
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Download cancelled by user');
    }
  }
});

cancelButton.addEventListener('click', () => {
  controller2.abort();
});

Promise.withResolvers() (ES2024)

طريقة حديثة لإنشاء Promises قابلة للحل (resolvable) دون الحاجة لتعيين متغيرات معقدة.

javascript
// Before ES2024 — verbose pattern with externally captured handlers
function createOldStyleDeferred() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

// ES2024 — same thing, built in
function createDeferred() {
  return Promise.withResolvers();
  // returns { promise, resolve, reject }
}

// Practical: wrapping event-driven code
async function waitForClick() {
  const { promise, resolve } = Promise.withResolvers();

  document.getElementById('button').addEventListener(
    'click',
    () => resolve('Button clicked!'),
    { once: true }
  );

  const result = await promise;
  console.log(result); // 'Button clicked!'
}

مقارنة: Callbacks مقابل Promises مقابل async/await

الجانبCallbacksPromisesasync/await
سهولة القراءةضعيفة (هرم الجحيم)جيدة (قابلة للتسلسل)ممتازة (متزامنة)
معالجة الأخطاءيدوية، ومملةمدمجة .catch()كتل try/catch
التكوين (Composition)صعبسلاسل .then()تتابعية أو Promise.all()
تصحيح الأخطاء (Debugging)غالباً ما يُفقد سياق العمليات غير المتزامنة عبر الـ callbacksأفضل، ولكن فقط عند التسلسل باستخدام .then()تتبع مكدس (stack traces) غير متزامن بدون تكلفة إضافية (Node 12+، V8 الحديث)
منحنى التعلمسهل في البداية، صعب عند التوسعمتوسطمتوسط
الأداءسريععبء إضافي طفيفنفس أداء Promises

أفضل الممارسات

١. تفضيل async/await على سلاسل .then() الطويلة

بالنسبة للمنطق غير المتزامن متعدد الخطوات، فإن async/await أسهل في القراءة وتنتج تتبع مكدس (stack traces) أنظف. ومع ذلك، لا تزال .then() صالحة تماماً — وأحياناً تكون أبسط — للتحويلات القصيرة ذات الخطوة الواحدة أو عند ربط الـ promises بكود غير متزامن (على سبيل المثال، إرجاع promise مباشرة من معالج المسار). اختر الأسلوب الذي يجعل تدفق البيانات واضحاً.

javascript
// Verbose chain — reads awkwardly past two steps
function getUser(id) {
  return fetch(`/api/users/${id}`)
    .then(r => r.json())
    .then(user => user);
}

// Cleaner with async/await
async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// Still fine: a single .then() that just unwraps JSON
const userJson = (id) => fetch(`/api/users/${id}`).then(r => r.json());

٢. معالجة الأخطاء بشكل صحيح

javascript
// Don't ignore errors
async function risky() {
  await someAsyncOperation(); // What if this throws?
}

// Do this
async function safe() {
  try {
    await someAsyncOperation();
  } catch (error) {
    console.error('Operation failed:', error);
    // Handle or re-throw
  }
}

٣. استخدام Promise.all() للعمليات المتوازية

javascript
// Don't do sequential when parallel is faster
async function slow(userId) {
  const user = await getUser(userId);
  const posts = await getPosts(userId);
  return { user, posts };
}

// Do this for parallelism
async function fast(userId) {
  const [user, posts] = await Promise.all([
    getUser(userId),
    getPosts(userId)
  ]);
  return { user, posts };
}

الخلاصة

يمثل التطور من الـ callbacks إلى الـ Promises ثم إلى async/await عقداً من التعلم في JavaScript. تظل الـ callbacks مفيدة للعمليات المتزامنة وطرق المصفوفات، وتشكل الـ Promises الأساس لـ JavaScript الحديث، بينما تعد async/await الطبقة المريحة فوقهما. أتقن الثلاثة — افهم لماذا يتم تحويل async/await إلى Promises، وكيف تدير الـ Promises المهام الدقيقة (microtasks)، ومتى تستخدم AbortController للإلغاء. بهذا الأساس، ستكتب JavaScript غير متزامن موثوق، سهل القراءة، وقابل للصيانة.

ابقَ على مسار النيرد

رسالة واحدة كلّ أسبوع: عدد الخميس يضمّ شرحًا لمنظومة واحدة، وثلاث إشارات، وتمرينًا واحدًا، ووظيفة واحدة، وخلاصة بالعربية.

بدون إزعاج. إلغاء الاشتراك في أي وقت.