レガシー非同期コードをAsync/Awaitに移行する

従来の非同期コードを本番環境を壊さずにAsync/Awaitに移行する

JavaScriptのコールバックモデルは、2009年にNode.jsが登場した当時、非ブロッキングI/Oを実現する唯一のメカニズムでした。ES6でPromiseが登場し、ES2017でasync/awaitがそれに続いた頃には、ほとんどのプロダクションコードベースには、すでに何年もコールバックベースのロジックが使われていました。ネストされたエラー優先ハンドラ、クロージャを介してスレッド化された共有可変状態、3階層も奥深く匿名関数に埋め込まれたリトライロジックなどです。構文は変わりましたが、実行中のコードは変わりませんでした。今日、ほとんどのエンジニアリングチームはまさにこの状況を引き継いでいます。新しいパターンと古いパターンが共存するコードベース、チームが移行したいと思っても、移行中にシステムを停止できない状況です。

非同期コードベースを安全に移行する

SMART TS XL そして、コードベース全体にわたる移行リスクを、コードを変更する前に特定します。

今すぐ探索する

朗報は、移行に書き換えが必要ないことです。コールバック、Promise、async/awaitは明確に定義された境界で相互運用可能です。Node.jsには util.promisify エラー優先のコールバックと Promise を返す関数の間のギャップを埋めるために、まさにこの仕組みが採用されています。ラッパーレイヤーによって、移行中に古いコードと新しいコードが共存できます。モジュールごとに段階的に移行することで、コードベースが進化する間も本番環境を稼働させ続けることができます。課題は変換そのものではなく、それを体系的に行うことです。どのコールバックを最初に変換しても安全か、どのアンチパターンを安易に書き換えるとバグが発生するか、そして変換された各関数が置き換えられた関数と全く同じように動作することをどのように検証するかを理解することが重要になります。

コールバックモデルの理解と、それが大規模化で破綻する理由

Node.jsのコールバックの慣習はシンプルです。非同期処理を行う関数は、最後の引数としてコールバックを受け取ります。コールバックは、最初の引数としてエラーを、2番目の引数として結果を受け取ります。Node.jsの標準ライブラリ関数はすべてこの慣習に従います。 fs.readFile, http.get, child_process.exec、その他数百件あります。

ジャバスクリプト

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

このパターンは、単一の操作であれば単純明快です。しかし、操作を連鎖させる必要がある場合は、各操作を前のコールバック内で開始する必要があるため、このパターンは破綻します。読み取り、変換、書き込みの 3 段階のシーケンスでは、3 つのネストされたコールバックが生成されます。5 段階のシーケンスでは、5 つのネストされたコールバックが生成されます。これは開発者が「コールバック地獄」または「破滅のピラミッド」と呼ぶ構造であり、単なる見た目の問題ではありません。深くネストされたコールバックは、エラー処理の一貫性を損ない(各レベルがエラー引数を個別にチェックする必要がある)、実行順序の推論を困難にし、データフローが明示的ではなく暗黙的であるため、リファクタリングを危険にします。

本番システムにとってより深刻な問題は、コールバックには調整のためのネイティブなメカニズムがないことです。2 つの非同期操作を実行して両方の完了を待つには、手動でカウンターを追跡する必要があります。配列に対して一連の操作を実行するには、再帰パターンまたは次のようなサードパーティライブラリが必要です。 asyncこうした複雑さは関数のシグネチャには一切現れません。コールバックベースのAPIと、その上に構築された並行オーケストレーションは、失敗するまでは外見上は全く同じに見えます。

コールバック地獄の実態

ユーザーデータを読み取り、権限を検証し、アクセスをログに記録するNode.jsサービスからの、実際のコールバックピラミッドの例:

ジャバスクリプト

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

エラー処理はあらゆるレベルで繰り返されます。インデントによってデータの流れが視覚的に複雑になります。4つ目のステップを追加するには、さらにネストされたレベルが必要になります。この関数をテストするには、3つの依存関係すべてを順番にモックする必要があり、3つ目のレベルでエラーをシミュレートするには、最初の2つの依存関係をモックして成功させる必要があります。これらの特性はすべて、チェーンが大きくなるにつれて悪化します。

util.promisify を使用してコールバックとプロミスを連携させる

Node.js 8.0 が導入されました util.promisifyこれは、標準的なエラー優先コールバック規約に従う関数を、Promiseを返す関数に変換します。これは、Node.jsコードベースにおけるasync/await移行の正しい出発点となります。

ジャバスクリプト

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 エラー優先の規約は自動的に処理されます。コールバックがnull以外の最初の引数を受け取った場合、返されるPromiseはそのエラーで拒否されます。コールバックがnullの最初の引数と結果を受け取った場合、Promiseは結果で解決されます。この規約に従うNode.jsコアAPIおよびサードパーティライブラリの大部分では、カスタムラッパーは必要ありません。

有望なカスタムコールバック関数

エラー優先の規約に従うが、Node.js の組み込み関数ではないカスタム関数については、 util.promisify 全く同じように機能します。

ジャバスクリプト

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 symbolを使用すると、カスタムの約束を定義できます。

ジャバスクリプト

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ラッパーが移行パスです。パターンは一貫しています。

ジャバスクリプト

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

先ほどの3段階コールバックピラミッドを、async/awaitを使って書き直したもの:

ジャバスクリプト

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

エラー処理は、各レベルで繰り返されるのではなく、呼び出し箇所での単一のtry/catchで処理されるようになりました。インデントはフラットです。4番目のステップを追加するには、さらに1つ必要です。 await 行。テストでは、各依存関係を個別に、任意の順序でモックする必要があります。

Promise.all と Promise.allSettled による並列実行

コールバックからasync/awaitへの移行時によくある間違いの一つは、並列実行可能な処理を逐次実行してしまうことです。コールバックでは並列実行が複雑になりすぎたため、多くの開発者はデフォルトで逐次的な処理チェーンを使用していました。async/awaitは並列処理を逐次的に見せてしまうため、開発者はコールバックを使用していた頃よりも遅いコードを書いてしまうという誤解を招く可能性があります。

ジャバスクリプト

// 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 正しいツールはこれです。

ジャバスクリプト

// 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 これは、従来の非同期コードベースにおいて、最も影響力の大きい変更の1つであることが多い。

await-in-loopアンチパターンを回避する

並列処理ではなく逐次処理をしてしまう間違いは、ループ内で最も頻繁に発生します。

ジャバスクリプト

// 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に移行する際に発生する最も一般的な不具合の1つです。これは、コールバックでは開発者が並行処理について明示的に考える必要があったのに対し、async/awaitではそれが隠蔽されてしまうためです。

Async/Awaitにおけるエラー処理:コールバックエラー伝播の置き換え

コールバックは慣例に従ってエラーを伝播します。つまり、すべてのコールバックの最初の引数はエラーまたはnullです。これは機能しますが、呼び出し元ごとにエラー引数を手動でチェックする必要があります。Async/awaitは、JavaScriptのネイティブなtry/catchと統合されたPromise拒否メカニズムを通じてエラーを伝播します。

ジャバスクリプト

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

移行時に重要な考慮事項となるのは、エラーコンテキストの保持です。コールバックベースのコードは、各レベルでコンテキスト情報が付加されたエラーを渡すことがよくあります。async/await に移行する際には、エラーのラップ処理がこのコンテキストを保持するようにしてください。

ジャバスクリプト

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

未処理の約束拒否

コールバックベースのコードは、エラー引数が無視された場合、エラーを黙って処理します。Async/await は未処理の Promise 拒否を生成し、Node.js 15 以降ではデフォルトでプロセスが終了します。これは移行時に互換性を損なう変更です。以前はエラーを黙って処理していたコードがクラッシュするようになります。

ジャバスクリプト

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

移行中に、すべてのファイアアンドフォーゲット非同期関数呼び出しを監査します。戻り値が待機またはチェーンされていない非同期関数への呼び出しはすべて .catch() コールバックの世界では潜在的なサイレントエラーだが、async/awaitの世界では未処理の拒否クラッシュとなる。

EventEmitterパターンをPromiseと非同期イテレータに移行する

Node.js の EventEmitter は、名前付きイベントに対して複数のコールバックを登録するコールバックパターンの一種です。ストリーム、ネットワーク接続、カスタムイベントバスなどでよく使用されます。async/await に直接移行するには、イベントベースの API をラップする必要があります。

ジャバスクリプト

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

一度限りのイベントをPromiseに変換するのは簡単です。

ジャバスクリプト

// 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:

ジャバスクリプト

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

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

Node.js の読み取り可能なストリームは、Node 10 以降、非同期イテラブルとして直接イテラブルにすることもできます。

ジャバスクリプト

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移行パターン

TypeScript コードベースでは、async/await への移行時に追加の考慮事項があります。戻り値の型は、コールバック シグネチャから更新する必要があります。 Promise<T>また、コンパイラは、await が async 関数内でのみ使用されることを強制します。

タイスクリプト

// 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 の厳密な null チェックは、一般的な移行エラーを検出する方法で非同期コードと連携します。 User | null コールバックを介して、移行によって変更されます Promise<User> (nullを返す代わりに例外をスローする)、TypeScriptはnullチェックをしていたがもはや必要なくなった呼び出し元と、エラーチェックをしていなかったが今では処理する必要がある呼び出し元を検出します。

レガシーな TypeScript コードを使用する場合 @types/node コールバックシグネチャ、 util.promisify 完全に型付けされており、Node.js の組み込み関数に対して適切な Promise の戻り値の型を自動的に推論します。

生産システム向け段階的移行戦略

本番システムを一度にすべて移行することはできません。段階的なアプローチでは、一度に1つのモジュールを変換し、検証してから次のモジュールに進みます。重要なのは、どの段階においても、変換済みコードと未変換コードの間に明確な境界を維持することです。

移行順序は依存関係の方向に従うべきです。まず最も深い依存関係を変換し、次に呼び出し元に向かって処理を進めます。これにより、上位レベルの関数が変換される時点で、その依存関係が既にPromiseを返すようになり、ラッパー層が不要になることが保証されます。

ジャバスクリプト

// 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: 非同期関数をエラー優先のコールバックインターフェースに変換します。

ジャバスクリプト

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

この双方向互換性により、変換作業は特別な日ではなく、いつでも実施できます。個々のモジュールは、すべての呼び出し元と同時に調整することなく、どのスプリントでも変換可能です。

移行時によくある非同期/待機の落とし穴

非同期関数呼び出しでawaitが欠落している

最もよくある移行エラーは、非同期関数をawaitせずに呼び出すことです。このエラーは、関数が拒否しない限りJavaScriptランタイムからは見えません。そして、拒否された場合、スローされるエラーではなく、未処理のPromise拒否として扱われます。

ジャバスクリプト

// 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 このルールは、このパターンを自動的に検出します。移行時にこのリンティングルールを追加することを強くお勧めします。

配列メソッドにおける非同期関数

Array.prototype.forEach 非同期コールバックを待機しません。これは、ループ内で待機する場合と同様に、順次実行されるものの誤った動作を引き起こしますが、すべてのコールバックを待機せずに並行して実行している間は、コードが正しく動作するように見えます。

ジャバスクリプト

// 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 ブロックでは拒否を捕捉されません。

ジャバスクリプト

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

移行後の非同期コードのテスト

移行された非同期関数には、非同期テストケースが必要です。最新のテストフレームワークは、これをネイティブにサポートしています。

ジャバスクリプト

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

移行された関数が、そのコールバックの前の関数と同一の出力を生成することを検証する場合、比較テストが有効です。

ジャバスクリプト

// 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 大規模な安全な非同期移行をサポート

コールバックチェーンが複数のファイル、サービス、チームにまたがるエンタープライズコードベースの場合、安全な移行のための最初の要件は、既存の非同期依存関係の完全なマップを作成することです。つまり、どの関数がどの関数を呼び出すか、それらの間でどのようなデータが渡されるか、どのチェーンがアプリケーションのコア操作にとって重要なパスであるか、そしてどのチェーンが独立して移行できる分離されたユーティリティであるかを把握することです。

SMART TS XL このツールは、個々のファイルだけでなくコードベース全体を解析することで、依存関係マップを構築します。モジュール境界を越えてコールバック関数がどのように相互参照しているかを解決し、クロージャを通してどの共有状態が引き継がれているかを特定し、移行後も維持しなければならない実行チェーンを視覚化します。この構造分析により、本ガイドで説明する段階的移行アプローチに必要な情報が得られます。つまり、依存関係グラフの最下層に位置し、最初に変換できるモジュールと、依存関係の移行が完了するまで待機しなければならないモジュールが特定されます。

プラットフォームの 影響分析 この機能は変更評価にも拡張されます。コールバックベースのモジュールを Promise を返すように変換する前に、影響分析によって、コールバックインターフェースを使用してそのモジュールを呼び出すコードベース内の他のすべてのモジュールが特定されます。これらの呼び出し元は次の段階の移行範囲になります。これらは同時に変換されるか、後方互換性のあるラッパーを使用して変換する必要があります。 util.callbackify 変換が完了するまで、このリストは維持されなければなりません。このリストがないと、移行は不明な範囲で進行し、識別されていない呼び出し元がコールバックを期待していた箇所でPromiseに遭遇した場合に、予期しない不具合が発生します。

JavaScriptとTypeScriptを混在させるコードベース、または他の言語のバックエンドサービスを呼び出すコードベースの場合、 SMART TS XLさん 言語間依存関係分析 JavaScript レイヤーだけでなく、実行パス全体を可視化します。Java または Python で記述された外部サービスへの呼び出しで終了するコールバックチェーンには、単一言語ツールでは認識できない依存関係があり、これらの依存関係を無視した移行計画は不完全です。 依存関係の可視化 それ SMART TS XL これにより、移行変更が行われる前に、それらの境界を越えた関係性を可視化できます。

移行の維持:コードベース全体におけるコールバックからAsync/Awaitへの移行

コールバックから async/await への移行は、最初のモジュールが変換された時点では完了しません。最後のラッパー層が削除され、コードベースに何も残っていない時点で完了します。 callback その中核となるロジックに規約が組み込まれている。そこに到達するには、移行期間全体を通して規律を守る必要がある。新しいコードはasync/awaitで記述し、ラッパーレイヤーは一時的なものとして扱い、ESLintルールによって変換されたモジュールにコールバック形式の関数が導入されないようにしなければならない。

移行完了の実際的な指標は次のとおりです。いいえ util.promisify アプリケーションコード内の呼び出し(移行期間のみ必要だった)、 (err, result) => コアビジネスロジックのパターン(非同期関数ではtry/catchに置き換えられています)、手動のPromiseコンストラクタはありません async/await それで十分であり、 Promise.all これまで独立した作業が順次行われていたすべての場所で。

これらはすべて静的解析によって測定可能であるため、進捗状況を推定ではなく客観的に追跡・報告できます。大規模なチームにとって、コールバックパターンを検出するための自動静的解析と、各移行ステージの範囲を定めるための依存関係解析を組み合わせることが、定められた期間内に完了する移行と、範囲が完全に把握されていないためにいつまでも終わらない移行との違いを生み出すのです。