העברת קוד אסינכרוני מדור קודם לקוד אסינכרוני/המתנה

העברת קוד אסינכרוני מדור קודם ל-Async/Await מבלי לשבור את Productionv

מודל ה-callback של JavaScript היה המנגנון היחיד ל-I/O ללא חסימה כאשר Node.js הופיע ב-2009. עד ש-Promises הגיע ל-ES6 ו-async/await הגיעו ב-ES2017, לרוב בסיסי הקוד של הייצור היו שנים של לוגיקה מבוססת callback שכבר הייתה בשירות: מטפלים מקוננים של שגיאות ראשונות, מצב משתנה משותף המושחל דרך סגירות, ולוגיקת ניסיון חוזר מוטמעת בפונקציות אנונימיות בעומק של שלוש רמות. התחביר השתנה. הקוד הפועל לא. כיום רוב צוותי ההנדסה יורשים בדיוק את המצב הזה: בסיס קוד שבו התבניות החדשות והישנות מתקיימות יחד, שבו הצוות רוצה להגר אבל המערכת לא יכולה לעצור בזמן שהם עושים זאת.

העבר את בסיס הקוד האסינכרוני שלך בצורה בטוחה

SMART TS XL ומזהה סיכון הגירה בכל בסיס הקוד שלך לפני שאתה משנה שורה.

גלה עכשיו

החדשות הטובות הן שההעברה אינה דורשת כתיבה מחדש. פעולות Callback, Promises ו-async/await פועלות באופן הדדי בגבולות מוגדרים היטב. Node.js נשלח util.promisify בדיוק כדי לגשר על הפער בין פונקציות החזרת שגיאות (error-first callbacks) לבין פונקציות החזרת הבטחות (Promise-returns). שכבות עוטפות מאפשרות לקוד ישן וחדש להתקיים יחד במהלך המעבר. הגירה מצטברת, מודול אחד בכל פעם, שומרת על הייצור פועל בזמן שבסיס הקוד מתקדם. האתגר אינו הטרנספורמציה עצמה, אלא ביצועה באופן שיטתי: הבנת אילו פונקציות החזרה בטוחות להמרה ראשונות, אילו תבניות אנטי-תבניות יגרמו לבאגים אם ייכתבו מחדש באופן נאיבי, וכיצד לאמת שכל פונקציה שהומרה מתנהגת באופן זהה לזו שהחליפה.

תוכן העניינים

הבנת מודל הקריאה החוזרת (Callback) ומדוע הוא מתקלקל בקנה מידה גדול

מוסכמת הקריאה החוזרת (callback) של Node.js היא פשוטה: פונקציות המבצעות עבודה אסינכרונית מקבלות קריאה חוזרת כארגומנט האחרון שלהן. הקריאה החוזרת מקבלת שגיאה כארגומנט הראשון שלה ואת התוצאה כארגומנט השני שלה. כל פונקציית ספרייה סטנדרטית ב-Node.js פועלת לפי מוסכמה זו: fs.readFile, http.get, child_process.execועוד מאות.

JavaScript

// Standard Node.js error-first callback pattern
const fs = require('fs');

fs.readFile('config.json', 'utf8', (err, data) => {
  if (err) {
    console.error('Failed to read config:', err);
    return;
  }
  const config = JSON.parse(data);
  console.log('Config loaded:', config);
});

דפוס זה פשוט לפעולה בודדת. הוא מתפרק כאשר יש לשרשר פעולות, מכיוון שכל פעולה עוקבת חייבת להיות מופעלת בתוך הקריאה החוזרת הקודמת. רצף בן שלושה שלבים של קריאה, המרה וכתיבה מייצר שלוש רמות מקוננות של קריאה חוזרת. רצף בן חמישה שלבים מייצר חמש. זהו המבנה שמפתחי ה-callback מכנים "גיהנום קריאה חוזרת" או "פירמידת האבדון", וזו לא רק בעיה אסתטית. קריאה חוזרת מקוננת עמוק הופכת את הטיפול בשגיאות לבלתי עקבי (כל רמה חייבת לבדוק באופן עצמאי את ארגומנט השגיאה שלה), מקשה על ההיגיון לגבי סדר הביצוע, והופכת את העיבוד מחדש למסוכן מכיוון שזרימת הנתונים מרומזת ולא מפורשת.

הבעיה הקריטית יותר עבור מערכות ייצור היא שלקריאה חוזרת אין מנגנון מקורי לתיאום. הרצת שתי פעולות אסינכרוניות והמתנה להשלמת שתיהן דורשת מעקב ידני אחר מונים. הרצת רצף של פעולות על פני מערך דורשת תבניות רקורסיביות או ספריות של צד שלישי כמו asyncשום דבר מהמורכבות הזו לא נראה בחתימות הפונקציה: API מבוסס callback ותזמור מקביל שנבנה עליו נראים זהים מבחוץ, עד שהם נכשלים.

איך נראה גיהנום של שיחות חוזרות באמת

פירמידת קריאה חוזרת בעולם האמיתי משירות Node.js שקוראת נתוני משתמש, מאמתת הרשאות ורושמת את הגישה:

JavaScript

// Three-level callback pyramid -- representative of real legacy code
function getUserReport(userId, callback) {
  db.query('SELECT * FROM users WHERE id = ?', [userId], (err, user) => {
    if (err) return callback(err);
    if (!user) return callback(new Error('User not found'));

    permissions.check(userId, 'read:reports', (err, allowed) => {
      if (err) return callback(err);
      if (!allowed) return callback(new Error('Permission denied'));

      auditLog.write({ userId, action: 'read:reports' }, (err) => {
        if (err) return callback(err);
        // Finally, the actual work
        callback(null, buildReport(user));
      });
    });
  });
}

טיפול בשגיאות חוזר בכל רמה. ההזחה הופכת את זרימת הנתונים למורכבת מבחינה ויזואלית. הוספת שלב רביעי דורשת רמה מקוננת נוספת. בדיקת פונקציה זו דורשת דמה את שלושת התלויות לפי הסדר, וסימולציה של שגיאה ברמה השלישית דורשת דמה את שתי הראשונות כדי להצליח. כל אחד מהמאפיינים הללו מחמיר ככל שהשרשרת גדלה.

שימוש ב- util.promisify כדי לגשר בין Callbacks והבטחות

Node.js 8.0 הוצג util.promisify, אשר ממירה כל פונקציה שעוקבת אחר מוסכמת הקריאה החוזרת הסטנדרטית של שגיאה ראשונה לפונקציה שמחזירה Promise. זוהי נקודת ההתחלה הנכונה לכל הגירה מסוג async/await בבסיס קוד של Node.js.

JavaScript

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

// Convert Node.js built-ins
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

// Now usable with async/await
async function processConfig(path) {
  const data = await readFile(path, 'utf8');
  const config = JSON.parse(data);
  config.lastLoaded = Date.now();
  await writeFile(path, JSON.stringify(config, null, 2), 'utf8');
  return config;
}

util.promisify מטפל באופן אוטומטי במוסכמה של error-first: אם ה-callback מקבל ארגומנט first שאינו null, ה-Promise המוחזר דוחה עם שגיאה זו. אם ה-callback מקבל ארגומנט first null ותוצאה, ה-Promise נפתר עם התוצאה. עבור הרוב המכריע של ממשקי ה-API של Node.js וספריות צד שלישי העוקבות אחר מוסכמה זו, אין צורך בעטיפה מותאמת אישית.

הבטחת פונקציות קריאה חוזרת מותאמות אישית

עבור פונקציות מותאמות אישית שעוקבות אחר מוסכמה של שגיאה ראשונה אך אינן מובנות ב- Node.js, util.promisify עובד באופן זהה:

JavaScript

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

// Your existing callback-based function
function fetchUserFromDB(userId, callback) {
  db.query('SELECT * FROM users WHERE id = ?', [userId], (err, rows) => {
    if (err) return callback(err);
    callback(null, rows[0] || null);
  });
}

// Promisified version -- no changes to the original function needed
const fetchUser = promisify(fetchUserFromDB);

// Use in async context
async function getUser(userId) {
  const user = await fetchUser(userId);
  if (!user) throw new Error(`User ${userId} not found`);
  return user;
}

גישה זו חשובה: util.promisify עוטף את הפונקציה המקורית מבלי לשנות אותה. גרסת הקריאה החוזרת המקורית ממשיכה לפעול. קוראים שטרם הועברו ממשיכים להשתמש בגרסת הקריאה החוזרת. קוראים שהועברו משתמשים בגרסה שהובטחה. דו-קיום זה הוא מה שמאפשר הגירה מצטברת.

טיפול בחתימות קריאה חוזרת לא סטנדרטיות

חלק מהספריות הישנות יותר מעבירות ערכי תוצאה מרובים לקריאות החוזרות שלהן, אשר util.promisify מתפרק לערך הראשון בלבד. במקרים אלה, ה- util.promisify.custom הסמל מאפשר הגדרת הבטחה מותאמת אישית:

JavaScript

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

// A function that passes two results to its callback
function parseData(input, callback) {
  // callback(err, parsedData, metadata)
  callback(null, { value: input.trim() }, { length: input.length });
}

// Custom promisification that returns both results
parseData[promisify.custom] = (input) => {
  return new Promise((resolve, reject) => {
    parseData(input, (err, data, meta) => {
      if (err) reject(err);
      else resolve({ data, meta });
    });
  });
};

const parseDataAsync = promisify(parseData);
const result = await parseDataAsync('  hello  ');
// result === { data: { value: 'hello' }, meta: { length: 9 } }

המרת שיחות חוזרות להבטחות: תבנית שלב אחר שלב

עבור קוד שלא ניתן לטפל בו על ידי util.promisify באופן ישיר, עטיפת ה-Promise הידנית היא נתיב ההגירה. הדפוס עקבי:

JavaScript

// Step 1: Original callback-based function
function checkPermission(userId, resource, callback) {
  acl.check({ userId, resource }, (err, result) => {
    if (err) return callback(err);
    callback(null, result.allowed);
  });
}

// Step 2: Promise wrapper (coexists with the original)
function checkPermissionAsync(userId, resource) {
  return new Promise((resolve, reject) => {
    checkPermission(userId, resource, (err, allowed) => {
      if (err) reject(err);
      else resolve(allowed);
    });
  });
}

// Step 3: async/await consumer
async function authorizeRequest(userId, resource) {
  const allowed = await checkPermissionAsync(userId, resource);
  if (!allowed) {
    throw new Error(`${userId} does not have access to ${resource}`);
  }
}

פירמידת הקריאה החוזרת בת שלוש הרמות מקודם, שנכתבה מחדש עם async/await:

JavaScript

// After migration: same logic, linear structure
async function getUserReport(userId) {
  const user = await db.queryAsync('SELECT * FROM users WHERE id = ?', [userId]);
  if (!user) throw new Error('User not found');

  const allowed = await permissions.checkAsync(userId, 'read:reports');
  if (!allowed) throw new Error('Permission denied');

  await auditLog.writeAsync({ userId, action: 'read:reports' });

  return buildReport(user);
}

טיפול השגיאות מטופל כעת על ידי ניסיון/תפיסה יחיד באתר הקריאה במקום לחזור על עצמו בכל רמה. ההזחה שטוחה. הוספת שלב רביעי דורשת שלב נוסף. await שורה. בדיקה דורשת לבצע לעג לכל תלות באופן עצמאי, בכל סדר שהוא.

ביצוע מקביל עם Promise.all ו- Promise.allSettled

אחת הטעויות הנפוצות ביותר בעת מעבר מ-callbacks ל-async/await היא ביצוע סדרתי של פעולות שיכולות לפעול במקביל. Callbacks הפכו את הביצוע המקביל למורכב מספיק עד שמפתחים רבים עברו כברירת מחדל לשרשראות סדרתיות. Async/await גורם למקבילות להיראות סדרתית, מה שיכול להטעות מפתחים לכתוב קוד איטי יותר מקודמו ב-callback.

JavaScript

// WRONG: sequential execution -- each awaits the previous result
async function loadDashboardData(userId) {
  const profile = await fetchProfile(userId);       // 100ms
  const orders  = await fetchOrders(userId);        // 150ms
  const reviews = await fetchReviews(userId);       // 80ms
  return { profile, orders, reviews };              // Total: ~330ms
}

// RIGHT: parallel execution with Promise.all
async function loadDashboardData(userId) {
  const [profile, orders, reviews] = await Promise.all([
    fetchProfile(userId),
    fetchOrders(userId),
    fetchReviews(userId),
  ]);
  return { profile, orders, reviews };              // Total: ~150ms
}

Promise.all דוחה אם Promise כלשהו במערך דוחה. כאשר פעולות עצמאיות יכולות להיכשל מבלי להשפיע זו על זו והקורא צריך לדעת על כל הכשלונות, Promise.allSettled הוא הכלי הנכון:

JavaScript

// Promise.allSettled: runs all, reports success or failure per operation
async function syncAllSources(userId) {
  const results = await Promise.allSettled([
    syncFromGitHub(userId),
    syncFromJira(userId),
    syncFromSlack(userId),
  ]);

  const failed = results
    .filter(r => r.status === 'rejected')
    .map(r => r.reason.message);

  if (failed.length > 0) {
    console.warn('Some syncs failed:', failed);
  }

  return results
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value);
}

לתבנית זו אין מקבילה טבעית בקוד מבוסס קריאה חוזרת ללא מונה ידני או ספרייה כמו async.parallelמעבר אל Promise.allSettled הוא לעתים קרובות אחד השינויים בעלי המינוף הגבוה ביותר בבסיס קוד אסינכרוני מדור קודם.

הימנעות מדפוס האנטי-דפוס של המתנה בלולאה

הטעות של סדרתי במקום מקבילי מופיעה לרוב בתוך לולאות:

JavaScript

// WRONG: sequential -- processes items one at a time
async function processOrders(orderIds) {
  const results = [];
  for (const id of orderIds) {
    const result = await processOrder(id);  // blocks until each completes
    results.push(result);
  }
  return results;
}

// RIGHT: parallel -- all orders processed concurrently
async function processOrders(orderIds) {
  return Promise.all(orderIds.map(id => processOrder(id)));
}

// RIGHT (with concurrency limit): parallel but bounded
const pLimit = require('p-limit');
const limit = pLimit(5);  // max 5 concurrent

async function processOrders(orderIds) {
  return Promise.all(
    orderIds.map(id => limit(() => processOrder(id)))
  );
}

תבנית await-in-loop היא אחת מהרגרסיות הנפוצות ביותר המוצגות במהלך הגירת async/await של קוד מבוסס callback, מכיוון ש-callbacks אילצו את המפתחים לחשוב על מקביליות במפורש בעוד ש-async/await מסתיר אותה.

טיפול בשגיאות ב-Async/Await: החלפת הפצת שגיאות Callback

Callbacks מפיצים שגיאות לפי מוסכמה: הארגומנט הראשון של כל callback הוא שגיאה או null. זה עובד אבל דורש מכל קורא לבדוק ידנית את ארגומנט השגיאה. Async/await מפיצים שגיאות דרך מנגנון דחיית Promise, שמשתלב עם try/catch המקורי של JavaScript.

JavaScript

// Callback error propagation: repeated at every level
function processPayment(orderId, callback) {
  validateOrder(orderId, (err, order) => {
    if (err) return callback(err);  // propagate
    chargeCard(order.amount, (err, charge) => {
      if (err) return callback(err);  // propagate again
      updateInventory(orderId, (err) => {
        if (err) return callback(err);  // propagate again
        callback(null, charge.id);
      });
    });
  });
}

// Async/await: error propagation is automatic
async function processPayment(orderId) {
  const order  = await validateOrder(orderId);   // throws on error
  const charge = await chargeCard(order.amount); // throws on error
  await updateInventory(orderId);                // throws on error
  return charge.id;
}

שיקול קריטי במהלך ההעברה הוא שימור הקשר השגיאות. קוד מבוסס קריאה חוזרת (callback) מעביר לעתים קרובות שגיאות שהוגדלו במידע הקשרי בכל רמה. בעת העברה ל-async/wait, יש לוודא שעטיפת השגיאות שומרת על הקשר זה:

JavaScript

// Preserving error context during async migration
async function processPayment(orderId) {
  let order;
  try {
    order = await validateOrder(orderId);
  } catch (err) {
    throw new Error(`Payment validation failed for order ${orderId}: ${err.message}`);
  }

  try {
    const charge = await chargeCard(order.amount);
    await updateInventory(orderId);
    return charge.id;
  } catch (err) {
    // Attempt rollback, then rethrow with context
    await refundCharge(order.amount).catch(console.error);
    throw new Error(`Payment processing failed for order ${orderId}: ${err.message}`);
  }
}

דחיות הבטחות שלא טופלו

קוד מבוסס Callback בולע שגיאות בשקט כאשר ארגומנט השגיאה מתעלם. Async/await מייצר דחיות Promise לא מטופלות, אשר ב-Node.js 15+ גורמות לסיום תהליך כברירת מחדל. זהו שינוי משמעותי בעת הגירה: קוד שנכשל בעבר בשקט יקרוס כעת.

JavaScript

// This produces an unhandled rejection in Node.js 15+
async function riskyOperation() {
  throw new Error('Something failed');
}

riskyOperation(); // Promise rejected, but rejection is not caught

// Fix: always await or chain .catch()
await riskyOperation();              // throws, caller handles it
riskyOperation().catch(console.error); // handles inline

ביקורת על כל קריאות הפונקציה האסינכרונית של "fire-and-forget" במהלך ההעברה. כל קריאה לפונקציה אסינכרונית שערך ההחזרה שלה אינו ממתין או מקושר אליה. .catch() הוא כשל שקט פוטנציאלי בעולם ה-callback אך קריסת דחייה לא מטופלת בעולם האסינכרוני/המתנה.

העברת תבניות EventEmitter ל-Promises ו-Async Iterators

Node.js EventEmitters הם סוג של תבנית callback שבה נרשמות מספר callbacks עבור אירועים בעלי שם. הם נפוצים בזרמים, חיבורי רשת ואפיקי אירועים מותאמים אישית. מעבר ישיר ל-async/await דורש עטיפת ה-API מבוסס האירועים.

JavaScript

const { EventEmitter } = require('events');

// Original EventEmitter-based pattern
function fetchDataLegacy(source) {
  const emitter = new EventEmitter();
  setTimeout(() => {
    emitter.emit('data', { records: [1, 2, 3] });
    emitter.emit('end');
  }, 100);
  return emitter;
}

// Usage: callback registration
const stream = fetchDataLegacy('api');
stream.on('data', chunk => console.log('received', chunk));
stream.on('error', err => console.error('error', err));
stream.on('end', () => console.log('done'));

המרת אירוע חד פעמי להבטחה היא פשוטה:

JavaScript

// Converting a single-event completion to Promise
function waitForEvent(emitter, successEvent, errorEvent = 'error') {
  return new Promise((resolve, reject) => {
    emitter.once(successEvent, resolve);
    emitter.once(errorEvent, reject);
  });
}

async function fetchData(source) {
  const emitter = fetchDataLegacy(source);
  const data = await waitForEvent(emitter, 'data');
  await waitForEvent(emitter, 'end');
  return data;
}

עבור זרמים שפולטים אירועי נתונים מרובים, Node.js מספק events.on אשר מחזיר איטרטור אסינכרוני, המאפשר לצרוך את הזרם המלא עם for await...of:

JavaScript

const { on } = require('events');

async function processStream(readable) {
  for await (const chunk of on(readable, 'data')) {
    await processChunk(chunk);
  }
}

זרמים קריאים של Node.js ניתנים גם לאיטרציה ישירה כזרמים אסינכרוניים איטרטיביים מאז Node 10:

JavaScript

const fs = require('fs');

async function countLines(filePath) {
  let lines = 0;
  const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
  for await (const chunk of stream) {
    lines += chunk.split('\n').length - 1;
  }
  return lines;
}

דפוסי הגירה אסינכרוניים/המתנה של TypeScript

לבסיסי קוד של TypeScript יש שיקולים נוספים בעת מעבר ל-async/await. יש לעדכן את סוגי ההחזרה מחתימות callback ל- Promise<T>, והמהדר אוכף ש-await משמש רק בתוך פונקציות אסינכרוניות.

כתב כתיבה

// Before: callback signature
function fetchUser(
  id: string,
  callback: (err: Error | null, user: User | null) => void
): void {
  db.findOne({ id }, callback);
}

// After: async/await signature with proper return type
async function fetchUser(id: string): Promise<User> {
  const user = await db.findOneAsync<User>({ id });
  if (!user) throw new Error(`User ${id} not found`);
  return user;
}

בדיקות ה-null הקפדניות של TypeScript מקיימות אינטראקציה עם קוד אסינכרוני באופן שתופס שגיאות העברה נפוצות. אם פונקציה הוחזרה בעבר User | null באמצעות קריאה חוזרת וההגירה משנה זאת ל Promise<User> (זריקת ערך במקום החזרת null), TypeScript יתפוס קוראים שבדקו null אך אינם צריכים עוד, וקוראים שלא בודקים שגיאות שעליהם לטפל בהן כעת.

עבור קוד TypeScript מדור קודם המשתמש ב- @types/node חתימות קריאה חוזרת, util.promisify מוקלד במלואו ומסיק את סוג החזרת ה-Promise הנכון עבור פונקציות מובנות של Node.js באופן אוטומטי.

אסטרטגיית הגירה הדרגתית עבור מערכות ייצור

לא ניתן להעביר מערכת ייצור בבת אחת. הגישה ההדרגתית ממירה מודול אחד בכל פעם, מאמתת אותו, ואז עוברת לקוד הבא. המפתח הוא שמירה על גבול נקי בין קוד שהומר לקוד שלא המומר בכל שלב.

סדר ההעברה צריך לעקוב אחר כיוון התלות: להמיר תחילה את התלויות העמוקות ביותר, ולאחר מכן לעבוד כלפי מעלה לכיוון הקוראים. זה מבטיח שעד שפונקציה ברמה גבוהה יותר תומר, התלויות שלה כבר מחזירות Promises, ושכבת העטיפה כבר לא נחוצה.

JavaScript

// Stage 1: Promisify the data layer (deepest dependency)
const db = {
  queryAsync: promisify(db.query.bind(db)),
  insertAsync: promisify(db.insert.bind(db)),
};

// Stage 2: Convert the service layer (depends on db)
class UserService {
  async getUser(id) {
    return db.queryAsync('SELECT * FROM users WHERE id = ?', [id]);
  }
  async createUser(data) {
    return db.insertAsync('users', data);
  }
}

// Stage 3: Convert the controller layer (depends on service)
// -- only after Stage 2 is validated and deployed
async function handleGetUser(req, res) {
  try {
    const user = await userService.getUser(req.params.id);
    res.json(user);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
}

גישה מדורגת זו נתמכת ישירות על ידי כלי ניתוח. כפי שנדון בניתוח זרימת נתונים ובקרה , הבנת האופן שבו נתונים זורמים דרך שכבות אסינכרוניות לפני שינוי שלהן היא תנאי מוקדם לעיבוד מחדש מצטבר בטוח. כיוון התלות קובע אילו מודולים בטוחים להמרה תחילה ואילו חייבים להמתין עד שהתלות שלהם כבר יועברו.

שכבות עוטפות לתאימות לאחור

במהלך ההעברה, חלק מהמתקשרים עדיין יצפו ל-API מבוססי התקשרות חוזרת. util.callbackify הפונקציה היא ההפוכה של util.promisify: זה ממיר פונקציית אסינכרון בחזרה לממשק קריאה חוזרת מסוג error-first:

JavaScript

const { callbackify } = require('util');

// New async implementation
async function fetchUserAsync(id) {
  return db.queryAsync('SELECT * FROM users WHERE id = ?', [id]);
}

// Backward-compatible callback version for unconverted callers
const fetchUser = callbackify(fetchUserAsync);

// Old callers continue to work unchanged
fetchUser(userId, (err, user) => {
  if (err) return handleError(err);
  render(user);
});

// New callers use the async version directly
const user = await fetchUserAsync(userId);

תאימות דו-כיוונית זו פירושה שההמרה אינה אירוע מיוחד. ניתן להמיר מודולים בודדים בכל ספרינט מבלי לתאם עם כל קורא בו זמנית.

מלכודות נפוצות של אסינכרון/המתנה במהלך הגירה

חסרה המתנה בקריאות לפונקציה אסינכרונית

שגיאת ההגירה הנפוצה ביותר היא קריאה לפונקציה אסינכרונית מבלי להמתין לה. שגיאת זו אינה נראית בזמן הריצה של JavaScript אלא אם כן הפונקציה דוחה אותה, והדחייה הופכת לדחיית Promise לא מטופלת במקום שגיאה שנזרקה.

JavaScript

// Bug: missing await -- function runs but result is a Promise, not the user
async function updateUserName(id, name) {
  const user = fetchUser(id);  // BUG: forgot await, user is a Promise object
  user.name = name;            // setting .name on a Promise, not a user
  await saveUser(user);        // saves the Promise object
}

// Fix
async function updateUserName(id, name) {
  const user = await fetchUser(id);
  user.name = name;
  await saveUser(user);
}

TypeScript ו-ESLint no-floating-promises כלל יתפוס דפוס זה באופן אוטומטי. מומלץ מאוד להוסיף כלל lint זה במהלך ההעברה.

פונקציות אסינכרוניות בשיטות מערך

Array.prototype.forEach לא ממתין לקריאות חוזרות אסינכרוניות. זה מייצר את אותה התנהגות סדרתית-אך-שגויה כמו await-in-loop, אלא שהקוד נראה עובד תוך כדי הרצה שקטה של ​​כל הקריאות החוזרות בו זמנית מבלי להמתין לאף אחת:

JavaScript

// Bug: forEach does not await async callbacks
async function processAll(ids) {
  ids.forEach(async (id) => {
    await processItem(id);  // these run concurrently, forEach completes immediately
  });
  // function returns before any processItem completes
}

// Fix: use Promise.all with map
async function processAll(ids) {
  await Promise.all(ids.map(id => processItem(id)));
}

Try/Catch לא תופס שגיאות אסינכרוניות מחוץ ל-Await

בלוק try/catch לוכד שגיאות רק מביטויים מסוג await. קריאה לפונקציה אסינכרונית ללא await לא תגרום לדחייה שלה להיקלט על ידי try/catch מסביב:

JavaScript

// Bug: the rejection from riskyOp() is not caught
async function run() {
  try {
    riskyOp();  // not awaited -- rejection escapes the try/catch
  } catch (err) {
    console.error(err);  // never reached
  }
}

// Fix: await inside the try block
async function run() {
  try {
    await riskyOp();
  } catch (err) {
    console.error(err);
  }
}

בדיקת קוד אסינכרוני לאחר הגירה

פונקציות אסינכרוניות שהועברו דורשות מקרי בדיקה אסינכרוניים. מסגרות בדיקה מודרניות תומכות בכך באופן טבעי.

JavaScript

// Jest async test patterns
describe('UserService', () => {
  // Pattern 1: async/await in test
  test('fetches user by id', async () => {
    const user = await userService.getUser('user-123');
    expect(user.id).toBe('user-123');
  });

  // Pattern 2: testing rejection
  test('throws when user not found', async () => {
    await expect(userService.getUser('nonexistent'))
      .rejects.toThrow('User nonexistent not found');
  });

  // Pattern 3: parallel setup
  beforeAll(async () => {
    await db.connect();
    await db.seed(testData);
  });

  afterAll(async () => {
    await db.cleanup();
    await db.disconnect();
  });
});

כאשר מאמתים שפונקציה שעברה העברה מייצרת פלט זהה לקודמתה בפונקציית הקריאה החוזרת, בדיקות השוואה יעילות:

JavaScript

// Comparison test: callback version vs. async version must agree
test('async version matches callback version output', async () => {
  const callbackResult = await promisify(fetchUserLegacy)('user-123');
  const asyncResult    = await fetchUserAsync('user-123');
  expect(asyncResult).toEqual(callbackResult);
});

תבנית בדיקה זו שימושית במיוחד בתקופת המעבר כאשר שתי הגרסאות פועלות במקביל. כפי שנבדק בהקשר של ניתוח השפעה לפני ביצוע שינויי קוד , אימות שפונקציה שעברה שיפוץ מייצרת פלטים זהים לקודמתה הוא אישור מבוסס ראיות לכך שההעברה נכונה לפני הסרת הגרסה הישנה.

איך SMART TS XL תומך בהגירה אסינכרונית בטוחה בקנה מידה גדול

עבור בסיסי קוד ארגוניים שבהם שרשראות callback משתרעות על פני קבצים, שירותים וצוותים מרובים, הדרישה הראשונה להעברה בטוחה היא מפה מלאה של התלויות האסינכרוניות הקיימות: אילו פונקציות קוראות לאילו, אילו נתונים עוברים ביניהן, אילו שרשראות הן נתיבים קריטיים לפעולות הליבה של היישום, ואילו הן כלי עזר מבודדים שניתן להעביר באופן עצמאי.

SMART TS XL בונה את מפת התלות הזו על ידי ניתוח כל בסיס הקוד, ולא רק קבצים בודדים. היא פותרת כיצד פונקציות מבוססות callback מתייחסות זו לזו על פני גבולות מודולים, מזהה איזה מצב משותף משורשר דרך סגירות, וממחישה את שרשראות הביצוע שחייבות להישאר שלמות במהלך ההגירה. ניתוח מבני זה מספק את הקלט לגישת ההגירה המדורגת המתוארת במדריך זה: אילו מודולים נמצאים בתחתית גרף התלות וניתן להמיר אותם ראשונים, ואילו מודולים חייבים להמתין עד שהתלות שלהם כבר יועברו.

הפלטפורמה ניתוח השפעות היכולת מרחיבה זאת להערכת שינויים. לפני המרת מודול מבוסס קריאה חוזרת להחזרת הבטחות, ניתוח ההשפעה מזהה כל מודול אחר בבסיס הקוד שקורא לו באמצעות ממשק קריאה חוזרת. קוראים אלה הם טווח ההעברה לשלב הבא: יש להמיר אותם בו זמנית, או עטיפה תואמת לאחור באמצעות util.callbackify יש לשמור אותם עד להמרה. ללא ספירה זו, העברות ממשיכות בהיקף לא ידוע ויוצרות שבירה בלתי צפויה כאשר קוראים שלא זוהו נתקלים בהבטחה שבה ציפו לקריאה חוזרת.

עבור בסיסי קוד המשלבים JavaScript עם TypeScript, או שקוראים לשירותי backend בשפות אחרות, SMART TS XL"S ניתוח תלות בין-לשונית מספק נראות לנתיב הביצוע המלא, לא רק לשכבת ה-JavaScript. שרשרת קריאה חוזרת שמסתיימת בקריאה לשירות חיצוני שנכתב ב-Java או Python כוללת תלויות שכלי עבודה בשפה אחת אינם יכולים לראות, ותכנון הגירה שמתעלם מתלות אלו אינו שלם. ויזואליזציה של תלות זֶה SMART TS XL provides הופך את קשרי הגומלין חוצי הגבולות הללו לגלויים לפני ביצוע שינויים כלשהם בהגירה.

שמירה על ההגירה: מ-Callback ל-Async/Await על פני כל בסיס הקוד

המעבר מ-callbacks ל-async/await אינו מושלם כאשר המודול הראשון מומר. הוא מושלם כאשר שכבת העטיפה האחרונה מוסרת ולבסיס הקוד אין קוד שנותר. callback מוסכמה בלוגיקה המרכזית שלה. הגעה לשם דורשת משמעת לאורך תקופת ההגירה: קוד חדש חייב להיכתב בצורה אסינכרונית/מחכה, שכבות עטיפה חייבות להיחשב זמניות, וכללי ESLint חייבים לאכוף שפונקציות בסגנון callback לא יוצגו במודולים שהומרו.

הסמנים המעשיים של הגירה שהושלמה הם: לא util.promisify קריאות בקוד האפליקציה (הן היו נחוצות רק לתקופת המעבר), לא (err, result) => דפוסים בלוגיקת העסקים המרכזית (הם הוחלפו על ידי try/catch בפונקציות אסינכרוניות), ללא בנאי Promise ידניים שבהם async/await יספיק, ו Promise.all בכל מקום בו פעולות עצמאיות נוהלו בעבר ברצף.

כל אחד מאלה ניתן למדידה באמצעות ניתוח סטטי, מה שאומר שניתן לעקוב אחר ההתקדמות ולדווח עליה באופן אובייקטיבי ולא באופן מוערך. עבור צוותים הפועלים בקנה מידה גדול, השילוב של ניתוח סטטי אוטומטי למציאת דפוסי קריאה חוזרת וניתוח תלות לקביעת היקף של כל שלב במעבר הוא מה שעושה את ההבדל בין מעבר שמסתיים בציר זמן מוגדר לבין מעבר שנמשך ללא הגבלת זמן משום שההיקף מעולם לא היה ידוע במלואו.