Eski Asenkron Kodun Async/Await'e Taşınması

Eski Asenkron Kodu Üretim Ortamını Bozmadan Async/Await'e Geçirme

JavaScript'in geri çağırma modeli, Node.js'nin 2009'da ortaya çıktığı dönemde engellemeyen G/Ç için tek mekanizmaydı. ES6'da Promise'ler ve ES2017'de async/await geldiğinde, çoğu üretim kod tabanında yıllardır geri çağırma tabanlı mantık zaten çalışıyordu: iç içe hata öncelikli işleyiciler, kapanışlar aracılığıyla paylaşılan değiştirilebilir durum, üç seviye derinliğe kadar anonim fonksiyonlara gömülü yeniden deneme mantığı. Sözdizimi değişti. Çalışan kod değişmedi. Bugün çoğu mühendislik ekibi tam olarak bu durumu devralıyor: yeni kalıpların ve eski kalıpların bir arada bulunduğu, ekibin geçiş yapmak istediği ancak bunu yaparken sistemin duramadığı bir kod tabanı.

Asenkron Kod Tabanınızı Güvenli Bir Şekilde Taşıyın

SMART TS XL ve tek bir satır bile değiştirmeden önce tüm kod tabanınızdaki geçiş riskini belirler.

Şimdi keşfedin

İyi haber şu ki, geçiş için yeniden yazmaya gerek yok. Geri çağrı fonksiyonları, Promise'ler ve async/await, iyi tanımlanmış sınırlar dahilinde birlikte çalışabilir. Node.js bunu destekliyor. util.promisify Tam olarak hata öncelikli geri çağırma fonksiyonları ile Promise döndüren fonksiyonlar arasındaki boşluğu kapatmak için. Sarmalayıcı katmanlar, geçiş sırasında eski ve yeni kodun bir arada var olmasına olanak tanır. Bir seferde bir modül olmak üzere artımlı geçiş, kod tabanı ilerlerken üretimin devam etmesini sağlar. Zorluk, dönüşümün kendisi değil, onu sistematik olarak yapmaktır: hangi geri çağırma fonksiyonlarının önce dönüştürülmesinin güvenli olduğunu, hangi anti-kalıpların safça yeniden yazılırsa hatalara neden olacağını ve dönüştürülen her fonksiyonun yerini aldığı fonksiyonla aynı şekilde davrandığını nasıl doğrulayacağınızı anlamak.

Geri Çağrı Modelini ve Büyük Ölçekte Neden Başarısız Olduğunu Anlamak

Node.js'in geri çağırma (callback) kuralı basittir: Asenkron işlem gerçekleştiren fonksiyonlar, son argüman olarak bir geri çağırma fonksiyonu kabul eder. Geri çağırma fonksiyonu, ilk argüman olarak hatayı, ikinci argüman olarak ise sonucu alır. Node.js'deki her standart kütüphane fonksiyonu bu kurala uyar: fs.readFile, http.get, child_process.execve yüzlercesi daha.

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);
});

Bu model tek bir işlem için basittir. Ancak işlemler zincirleme olarak gerçekleştirildiğinde bozulur, çünkü her sonraki işlem önceki geri çağırma fonksiyonunun içinde başlatılmalıdır. Okuma, dönüştürme ve yazma olmak üzere üç adımlı bir dizi, üç iç içe geçmiş geri çağırma seviyesi üretir. Beş adımlı bir dizi ise beş seviye üretir. Geliştiricilerin "geri çağırma cehennemi" veya "kıyamet piramidi" olarak adlandırdığı bu yapı, sadece estetik bir sorun değildir. Derinlemesine iç içe geçmiş geri çağırma fonksiyonları, hata işlemeyi tutarsız hale getirir (her seviye kendi hata argümanını bağımsız olarak kontrol etmelidir), yürütme sırasını anlamayı zorlaştırır ve veri akışı açık değil örtük olduğu için yeniden düzenlemeyi tehlikeli hale getirir.

Üretim sistemleri için daha kritik sorun, geri çağırma fonksiyonlarının (callbacks) koordinasyon için yerleşik bir mekanizmaya sahip olmamasıdır. İki eşzamansız işlemi çalıştırmak ve her ikisinin de tamamlanmasını beklemek, manuel sayaç takibi gerektirir. Bir dizi üzerinde bir dizi işlem çalıştırmak, özyinelemeli kalıplar veya üçüncü taraf kütüphaneler gerektirir. asyncBu karmaşıklığın hiçbiri fonksiyon imzalarında görünmez: geri çağırma tabanlı bir API ve bunun üzerine kurulu eşzamanlı bir düzenleme, başarısız olana kadar dışarıdan bakıldığında tamamen aynı görünür.

Geri Arama Cehennemi Gerçekte Nasıl Bir Şey?

Kullanıcı verilerini okuyan, izinleri doğrulayan ve erişimi kaydeden bir Node.js servisinden gerçek dünya geri çağırma piramidi:

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));
      });
    });
  });
}

Hata yönetimi her seviyede tekrarlanır. Girintiler, veri akışını görsel olarak karmaşık hale getirir. Dördüncü bir adım eklemek, başka bir iç içe geçmiş seviye gerektirir. Bu fonksiyonu test etmek, üç bağımlılığın da sırayla taklit edilmesini gerektirir ve üçüncü seviyede bir hatayı simüle etmek, ilk ikisinin başarılı bir şekilde taklit edilmesini gerektirir. Zincir büyüdükçe bu özelliklerin her biri daha da kötüleşir.

util.promisify kullanarak Geri Çağrı Fonksiyonları ve Promise'ler arasında köprü kurma

Node.js 8.0 tanıtıldı util.promisifyBu, standart hata öncelikli geri çağırma kuralını izleyen herhangi bir fonksiyonu Promise döndüren bir fonksiyona dönüştürür. Bu, Node.js kod tabanındaki herhangi bir async/await geçişi için doğru başlangıç ​​noktasıdır.

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 Hata öncelikli yaklaşımı otomatik olarak ele alır: geri çağrı fonksiyonu boş olmayan bir ilk argüman alırsa, döndürülen Promise bu hatayla reddedilir. Geri çağrı fonksiyonu boş bir ilk argüman ve bir sonuç alırsa, Promise sonuçla çözümlenir. Bu kuralı izleyen Node.js çekirdek API'lerinin ve üçüncü taraf kütüphanelerin büyük çoğunluğu için özel bir sarmalama gerekmez.

Özel Geri Çağrı Fonksiyonlarının Umut Vadedenleri

Hata öncelikli yaklaşımı izleyen ancak Node.js'nin yerleşik fonksiyonları olmayan özel fonksiyonlar için, util.promisify Aynı şekilde çalışır:

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;
}

Bu yaklaşım önemlidir: util.promisify Orijinal fonksiyonu değiştirmeden sarmalar. Orijinal geri çağırma sürümü çalışmaya devam eder. Henüz geçiş yapmamış çağırıcılar geri çağırma sürümünü kullanmaya devam eder. Geçiş yapmış çağırıcılar ise promise tabanlı sürümü kullanır. Bu birlikte varoluş, artımlı geçişi mümkün kılan şeydir.

Standart Dışı Geri Arama İmzalarının İşlenmesi

Bazı eski kütüphaneler, geri çağırma işlevlerine birden fazla sonuç değeri iletir; util.promisify Yalnızca ilk değere dönüşür. Bu durumlarda, util.promisify.custom Bu sembol, özel bir taahhüt tanımlamaya olanak tanır:

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 } }

Geri Çağrı Fonksiyonlarını Promise'lere Dönüştürme: Adım Adım Yöntem

İşlenemeyen kodlar için util.promisify Doğrudan doğruya, manuel Promise sarmalayıcısı geçiş yoludur. Desen tutarlıdır:

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}`);
  }
}

Daha önce bahsettiğimiz üç seviyeli geri çağırma piramidi, async/await kullanılarak yeniden yazıldı:

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);
}

Hata yönetimi artık her seviyede tekrarlanmak yerine, çağrı noktasındaki tek bir try/catch bloğu ile gerçekleştiriliyor. Girinti düz. Dördüncü bir adım eklemek bir adım daha gerektiriyor. await Test işlemi, her bir bağımlılığın bağımsız olarak, herhangi bir sırayla taklit edilmesini gerektirir.

Promise.all ve Promise.allSettled ile Paralel Yürütme

Geri çağrı fonksiyonlarından (callbacks) async/await'e geçişte yapılan en yaygın hatalardan biri, paralel olarak çalıştırılabilecek işlemlerin ardışık olarak yürütülmesidir. Geri çağrı fonksiyonları, paralel yürütmeyi o kadar karmaşık hale getirdi ki, birçok geliştirici varsayılan olarak ardışık zincirlere yöneldi. Async/await, paralelliği ardışık gibi gösterir; bu da geliştiricileri, önceki geri çağrı fonksiyonuna göre daha yavaş kod yazmaya yönlendirebilir.

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 Dizideki herhangi bir Promise reddedilirse, bu işlem de reddedilir. Bağımsız işlemler birbirini etkilemeden başarısız olabiliyorsa ve çağıranın tüm başarısızlıklar hakkında bilgi sahibi olması gerekiyorsa, Promise.allSettled doğru araç şudur:

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);
}

Bu kalıbın, manuel sayaç veya benzeri bir kütüphane olmadan, geri çağırma tabanlı kodda doğal bir karşılığı yoktur. async.parallel. Göç etmek Promise.allSettled Eski bir asenkron kod tabanındaki değişiklikler arasında genellikle en yüksek etkiye sahip olanlardan biridir.

Await-in-loop Anti-Pattern'inden Kaçınma

Sıralı yerine paralel kullanma hatası en sık döngülerin içinde görülür:

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)))
  );
}

Döngü içinde bekleme (await-in-loop) kalıbı, geri çağrı tabanlı kodlarda async/await kullanımına geçiş sırasında ortaya çıkan en yaygın gerilemelerden biridir; çünkü geri çağrılar geliştiricileri eşzamanlılığı açıkça düşünmeye zorlarken, async/await bunu gizler.

Async/Await'te Hata Yönetimi: Geri Çağrı Hata Yayılımının Değiştirilmesi

Geri çağrı fonksiyonları, hataları geleneksel olarak yayar: her geri çağrı fonksiyonunun ilk argümanı bir hata veya null'dur. Bu çalışır ancak her çağırıcının hata argümanını manuel olarak kontrol etmesini gerektirir. Async/await ise hataları, JavaScript'in yerel try/catch yapısıyla entegre olan Promise reddetme mekanizması aracılığıyla yayar.

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;
}

Geçiş sırasında kritik bir husus, hata bağlamının korunmasıdır. Geri çağrı tabanlı kodlar genellikle her seviyede bağlamsal bilgilerle zenginleştirilmiş hataları iletir. Async/await'e geçiş yaparken, hata sarmalama işleminin bu bağlamı koruduğundan emin olun:

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}`);
  }
}

Ele Alınamayan Vaat Redleri

Geri çağrı tabanlı kod, hata argümanı göz ardı edildiğinde hataları sessizce yutar. Async/await, Node.js 15 ve üzeri sürümlerde varsayılan olarak işlem sonlandırmasına neden olan, ele alınmamış Promise reddi üretir. Bu, geçiş sırasında önemli bir değişikliktir: daha önce sessizce başarısız olan kod artık çökecektir.

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

Geçiş sırasında tüm "ateşle ve unut" asenkron fonksiyon çağrılarını denetleyin. Dönüş değeri beklenmeyen veya zincirleme olarak bağlanmayan herhangi bir asenkron fonksiyon çağrısı denetlenmelidir. .catch() Bu, callback dünyasında potansiyel bir sessiz hata iken, async/await dünyasında ele alınmayan bir reddedilme çökmesidir.

EventEmitter Kalıplarını Promise'lere ve Asenkron Yineleyicilere Geçirme

Node.js EventEmitters, adlandırılmış olaylar için birden fazla geri çağırma fonksiyonunun kaydedildiği bir geri çağırma modelidir. Akışlarda, ağ bağlantılarında ve özel olay veri yollarında yaygındırlar. Async/await'e doğrudan geçiş, olay tabanlı API'nin sarmalanmasını gerektirir.

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'));

Tek seferlik bir olayı Promise'e dönüştürmek oldukça basittir:

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;
}

Birden fazla veri olayı yayan akışlar için Node.js şunları sağlar: events.on Bu, eşzamansız bir yineleyici döndürerek tüm akışın tüketilmesine olanak tanır. 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'in okunabilir akışları, Node 10'dan itibaren doğrudan eşzamansız yineleyiciler olarak da yinelenebilir:

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 Async/Await Geçiş Kalıpları

TypeScript kod tabanlarında async/await'e geçiş yaparken ek hususlar dikkate alınmalıdır. Dönüş türleri, callback imzalarından güncellenmelidir. Promise<T>Ayrıca derleyici, `await` ifadesinin yalnızca async fonksiyonların içinde kullanılmasını zorunlu kılar.

daktilo ile yazılmış yazı

// 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;
}

TypeScript'in katı null kontrolleri, yaygın geçiş hatalarını yakalayacak şekilde eşzamansız kodla etkileşime girer. Eğer bir fonksiyon daha önce bir değer döndürdüyse... User | null bir geri çağırma yoluyla ve geçiş işlemi bunu değiştirir. Promise<User> (null döndürmek yerine hata fırlatarak), TypeScript, null kontrolü yapmış ancak artık yapması gerekmeyen çağrıları ve artık ele alması gereken hataları kontrol etmeyen çağrıları yakalayacaktır.

Eski TypeScript kodları için, aşağıdaki kodu kullananlar için: @types/node geri çağrı imzaları, util.promisify Tamamen tiplendirilmiştir ve Node.js'nin yerleşik fonksiyonları için doğru Promise dönüş tipini otomatik olarak çıkarır.

Üretim Sistemleri için Aşamalı Geçiş Stratejisi

Üretim sistemi tek seferde tamamen taşınamaz. Artımlı yaklaşım, her seferinde bir modülü dönüştürür, doğrular ve ardından bir sonrakine geçer. Önemli olan, her aşamada dönüştürülen ve dönüştürülmeyen kod arasında net bir sınır korumaktır.

Geçiş sırası bağımlılık yönünü takip etmelidir: önce en derin bağımlılıklar dönüştürülmeli, ardından çağıranlara doğru yukarı doğru çalışılmalıdır. Bu, daha üst düzey bir fonksiyon dönüştürüldüğünde, bağımlılıklarının zaten Promise döndürüyor olmasını ve sarmalayıcı katmanın artık gerekli olmamasını sağlar.

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 });
  }
}

Bu aşamalı yaklaşım, analiz araçları tarafından doğrudan desteklenmektedir. Veri ve kontrol akışı analizinde tartışıldığı gibi , veri akışının eşzamansız katmanlardan nasıl geçtiğini anlamak, güvenli artımlı yeniden düzenleme için ön koşuldur. Bağımlılık yönü, hangi modüllerin önce dönüştürülmesinin güvenli olduğunu ve hangilerinin bağımlılıkları zaten taşınana kadar beklemesi gerektiğini belirler.

Geriye Dönük Uyumluluk için Sarmalayıcı Katmanlar

Geçiş sırasında, bazı arayanlar hala geri çağrı tabanlı API'ler bekleyecektir. util.callbackify fonksiyon, tersidir util.promisifyBu, eşzamansız bir fonksiyonu hata öncelikli geri çağırma arayüzüne dönüştürür:

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);

Bu çift yönlü uyumluluk, dönüşümün son güne özgü bir olay olmadığı anlamına gelir. Bireysel modüller, her bir çağrı yapanla aynı anda koordinasyon sağlamaya gerek kalmadan herhangi bir sprint'te dönüştürülebilir.

Geçiş Sırasında Sık Karşılaşılan Async/Await Hataları

Asenkron fonksiyon çağrılarında `await` eksikliği

En sık karşılaşılan geçiş hatası, bekleme (await) işlemi yapılmadan eşzamansız (async) bir fonksiyonun çağrılmasıdır. Bu durum, fonksiyon reddedilmediği sürece JavaScript çalışma zamanı için görünmezdir ve reddedilme, fırlatılan bir hata yerine, ele alınmayan bir Promise reddi olarak gerçekleşir.

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 ve ESLint'in no-floating-promises Bu kural, bu deseni otomatik olarak yakalar. Bu lint kuralını geçiş sırasında eklemeniz şiddetle tavsiye edilir.

Dizi Metotlarında Asenkron Fonksiyonlar

Array.prototype.forEach Asenkron geri çağırmaları beklemez. Bu, döngü içinde bekleme ile aynı sıralı ancak yanlış davranışı üretir; ancak kod, hiçbirini beklemeden tüm geri çağırmaları eş zamanlı olarak sessizce çalıştırırken sorunsuz çalışıyor gibi görünür:

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 dışındaki asenkron hataları yakalamaz.

Bir try/catch bloğu yalnızca await içeren ifadelerden kaynaklanan hataları yakalar. Await içermeyen bir async fonksiyon çağrısının reddedilmesi, çevreleyen try/catch bloğu tarafından yakalanmaz:

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);
  }
}

Taşıma Sonrası Asenkron Kodun Test Edilmesi

Taşınan asenkron fonksiyonlar asenkron test senaryoları gerektirir. Modern test çerçeveleri bunu doğal olarak destekler.

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();
  });
});

Taşınan bir fonksiyonun, kendisinden önceki geri çağrı fonksiyonuyla aynı çıktıyı ürettiğini doğrulamak için karşılaştırma testi etkilidir:

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);
});

Bu test modeli, her iki sürümün de paralel olarak çalıştığı geçiş döneminde özellikle kullanışlıdır. Kod değişiklikleri yapılmadan önce etki analizi bağlamında incelendiğinde , yeniden düzenlenmiş bir fonksiyonun öncekiyle aynı çıktıları ürettiğinin doğrulanması, eski sürüm kaldırılmadan önce geçişin doğru olduğuna dair kanıta dayalı bir teyittir.

Ne kadar SMART TS XL Büyük Ölçekte Güvenli Asenkron Geçişi Destekler

Geri çağrı zincirlerinin birden fazla dosya, servis ve ekip arasında yayıldığı kurumsal kod tabanlarında, güvenli geçiş için ilk şart, mevcut eşzamansız bağımlılıkların eksiksiz bir haritasıdır: hangi fonksiyonlar hangilerini çağırır, aralarında hangi veriler aktarılır, hangi zincirler uygulamanın temel işlemleri için kritik yollardır ve hangileri bağımsız olarak taşınabilen izole yardımcı programlardır.

SMART TS XL Bu bağımlılık haritası, yalnızca tek tek dosyaları değil, tüm kod tabanını ayrıştırarak oluşturulur. Geri çağrı tabanlı fonksiyonların modül sınırları boyunca birbirlerine nasıl referans verdiğini çözer, hangi paylaşılan durumun kapatmalar aracılığıyla aktarıldığını belirler ve geçiş sırasında bozulmadan kalması gereken yürütme zincirlerini görselleştirir. Bu yapısal analiz, bu kılavuzda açıklanan aşamalı geçiş yaklaşımı için girdi sağlar: bağımlılık grafiğinin en altında hangi modüllerin olduğunu ve önce hangilerinin dönüştürülebileceğini ve hangi modüllerin bağımlılıkları zaten geçirilene kadar beklemesi gerektiğini belirler.

Platformun etki analizi Bu yetenek, değişiklik değerlendirmesine kadar uzanır. Geri çağrı tabanlı bir modülü Promise döndürecek şekilde dönüştürmeden önce, etki analizi, kod tabanında onu geri çağrı arayüzüyle çağıran diğer tüm modülleri belirler. Bu çağırıcılar, bir sonraki aşama için geçiş kapsamını oluşturur: bunlar ya eş zamanlı olarak dönüştürülmeli ya da geriye dönük uyumlu bir sarmalayıcı kullanılarak dönüştürülmelidir. util.callbackify Dönüştürülene kadar korunmaları gerekir. Bu numaralandırma olmadan, geçişler bilinmeyen bir kapsamla ilerler ve tanımlanmamış arayanlar bir geri çağırma beklerken bir Promise ile karşılaştıklarında beklenmedik arızalar meydana gelir.

JavaScript'i TypeScript ile birleştiren veya diğer dillerdeki arka uç servislerini çağıran kod tabanları için, SMART TS XL'S diller arası bağımlılık analizi Bu, yalnızca JavaScript katmanını değil, tüm yürütme yolunu görünür kılar. Java veya Python ile yazılmış harici bir servise yapılan çağrıyla sonlanan bir geri çağırma zincirinin, tek dilli araçların göremediği bağımlılıkları vardır ve bu bağımlılıkları göz ardı eden geçiş planlaması eksiktir. bağımlılık görselleştirmesi o SMART TS XL Bu özellik, herhangi bir geçiş değişikliği yapılmadan önce sınır ötesi ilişkileri görünür hale getirir.

Geçişin Sürdürülmesi: Tüm Kod Tabanında Geri Çağrıdan Async/Await'e Geçiş

Geri çağrı fonksiyonlarından async/await'e geçiş, ilk modül dönüştürüldüğünde tamamlanmaz. Bu geçiş, son sarmalayıcı katman kaldırıldığında ve kod tabanında hiçbir şey kalmadığında tamamlanır. callback Temel mantığı itibariyle geleneksel bir yaklaşımdır. Bu noktaya ulaşmak, geçiş süreci boyunca disiplin gerektirir: yeni kod async/await ile yazılmalı, sarmalayıcı katmanlar geçici olarak ele alınmalı ve ESLint kuralları, dönüştürülen modüllere geri çağrı tarzı fonksiyonların eklenmemesini sağlamalıdır.

Göçün tamamlandığının pratik göstergeleri şunlardır: yok util.promisify Uygulama kodundaki çağrılar (sadece geçiş dönemi için gerekliydiler), hayır. (err, result) => Temel iş mantığındaki kalıplar (async fonksiyonlarda try/catch ile değiştirildiler), manuel Promise kurucularının olmaması. async/await yeterli olurdu ve Promise.all Daha önce bağımsız işlemlerin ardışık olarak yürütüldüğü her yerde.

Bunların her biri statik analiz yoluyla ölçülebilir; bu da ilerlemenin tahmine dayalı olmaktan ziyade objektif olarak izlenebileceği ve raporlanabileceği anlamına gelir. Büyük ölçekte çalışan ekipler için, geri çağırma kalıplarını bulmak için otomatikleştirilmiş statik analiz ve her geçiş aşamasının kapsamını belirlemek için bağımlılık analizinin birleşimi, tanımlanmış bir zaman çizelgesinde tamamlanan bir geçiş ile kapsamı hiçbir zaman tam olarak bilinmediği için süresiz olarak devam eden bir geçiş arasındaki farkı yaratır.