Migración de código asíncrono heredado a Async/Await

Migración de código asíncrono heredado a Async/Await sin interrumpir la producción.

El modelo de devolución de llamada de JavaScript era el único mecanismo para E/S no bloqueante cuando Node.js apareció en 2009. Para cuando las Promises llegaron en ES6 y async/await en ES2017, la mayoría de los códigos de producción ya tenían años de lógica basada en devoluciones de llamada en funcionamiento: manejadores de errores anidados, estado mutable compartido encadenado a través de cierres, lógica de reintento integrada en funciones anónimas de tres niveles de profundidad. La sintaxis cambió. El código en ejecución no. Hoy en día, la mayoría de los equipos de ingeniería heredan exactamente esta situación: un código donde coexisten los patrones nuevos y los antiguos, donde el equipo quiere migrar pero el sistema no puede detenerse mientras lo hacen.

Migra tu código base asíncrono de forma segura.

SMART TS XL e identifica el riesgo de migración en todo el código fuente antes de que modifiques una sola línea.

Explora ahora

La buena noticia es que la migración no requiere reescritura. Las devoluciones de llamada, las promesas y async/await son interoperables en límites bien definidos. Node.js incluye util.promisify Precisamente para salvar la brecha entre las funciones de devolución de llamada que manejan errores y las funciones que devuelven promesas. Las capas de envoltura permiten que el código antiguo y el nuevo coexistan durante la transición. La migración incremental, módulo por módulo, mantiene la producción en marcha mientras el código base avanza. El desafío no reside en la transformación en sí, sino en realizarla sistemáticamente: comprender qué funciones de devolución de llamada se pueden convertir primero de forma segura, qué antipatrones causarán errores si se reescriben ingenuamente y cómo validar que cada función convertida se comporte de forma idéntica a la que reemplazó.

Comprender el modelo de devolución de llamada y por qué falla a gran escala.

La convención de devolución de llamada de Node.js es sencilla: las funciones que realizan trabajo asíncrono aceptan una devolución de llamada como último argumento. La devolución de llamada recibe un error como primer argumento y el resultado como segundo. Todas las funciones de la biblioteca estándar de Node.js siguen esta convención: fs.readFile, http.get, child_process.exec, y cientos más.

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

Este patrón es sencillo para una sola operación. Se complica cuando las operaciones deben encadenarse, ya que cada operación subsiguiente debe iniciarse dentro de la función de devolución de llamada anterior. Una secuencia de tres pasos (lectura, transformación y escritura) genera tres niveles anidados de funciones de devolución de llamada. Una secuencia de cinco pasos genera cinco. Esta es la estructura que los desarrolladores denominan «infierno de las funciones de devolución de llamada» o «pirámide de la perdición», y no se trata solo de un problema estético. Las funciones de devolución de llamada profundamente anidadas generan inconsistencias en el manejo de errores (cada nivel debe verificar de forma independiente su argumento de error), dificultan el razonamiento sobre el orden de ejecución y hacen que la refactorización sea peligrosa, ya que el flujo de datos es implícito en lugar de explícito.

El problema más crítico para los sistemas de producción es que las devoluciones de llamada no tienen un mecanismo nativo para la coordinación. Ejecutar dos operaciones asíncronas y esperar a que ambas se completen requiere un seguimiento manual del contador. Ejecutar una secuencia de operaciones sobre una matriz requiere patrones recursivos o bibliotecas de terceros como asyncNinguna de estas complejidades es visible en las firmas de las funciones: una API basada en devoluciones de llamada y una orquestación concurrente construida sobre ella parecen idénticas desde fuera, hasta que fallan.

Cómo es realmente el infierno de las devoluciones de llamada

Una pirámide de devolución de llamada real de un servicio Node.js que lee datos de usuario, valida permisos y registra el acceso:

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

El manejo de errores se repite en cada nivel. La indentación hace que el flujo de datos sea visualmente complejo. Agregar un cuarto paso requiere otro nivel anidado. Probar esta función requiere simular las tres dependencias en orden, y simular un error en el tercer nivel requiere simular las dos primeras para tener éxito. Cada una de estas características empeora a medida que la cadena crece.

Utilizar util.promisify para conectar callbacks y promesas

Se introdujo Node.js 8.0 util.promisify, que convierte cualquier función que siga la convención estándar de devolución de llamada con el error primero en una función que devuelve una Promesa. Este es el punto de partida correcto para cualquier migración async/await en un código base de 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 Gestiona automáticamente la convención de error primero: si la función de devolución de llamada recibe un primer argumento no nulo, la promesa devuelta se rechaza con ese error. Si la función de devolución de llamada recibe un primer argumento nulo y un resultado, la promesa se resuelve con el resultado. Para la gran mayoría de las API principales de Node.js y las bibliotecas de terceros que siguen esta convención, no se necesita ninguna adaptación personalizada.

Funciones de devolución de llamada personalizadas prometedoras

Para funciones personalizadas que siguen la convención de error-primero pero no son funciones integradas de Node.js, util.promisify funciona de forma idéntica:

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

Este enfoque es importante: util.promisify Envuelve la función original sin modificarla. La versión original con devolución de llamada sigue funcionando. Quienes aún no han migrado siguen utilizando la versión con devolución de llamada. Quienes ya han migrado utilizan la versión con promesa. Esta coexistencia es lo que posibilita la migración incremental.

Manejo de firmas de devolución de llamada no estándar

Algunas bibliotecas más antiguas pasan múltiples valores de resultado a sus funciones de devolución de llamada, que util.promisify se resuelve en el primer valor solamente. Para estos casos, el util.promisify.custom El símbolo permite definir una promesa personalizada:

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

Conversión de funciones de devolución de llamada a promesas: El patrón paso a paso

Para el código que no puede ser manejado por util.promisify Directamente, el envoltorio manual de Promise es la ruta de migración. El patrón es consistente:

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

La pirámide de devolución de llamada de tres niveles anterior, reescrita con 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);
}

El manejo de errores ahora se maneja mediante un único try/catch en el sitio de llamada en lugar de repetirse en cada nivel. La sangría es plana. Agregar un cuarto paso requiere uno más await línea. Las pruebas requieren simular cada dependencia de forma independiente, en cualquier orden.

Ejecución paralela con Promise.all y Promise.allSettled

Uno de los errores más comunes al migrar de callbacks a async/await es la ejecución secuencial de operaciones que podrían ejecutarse en paralelo. Los callbacks complicaban tanto la ejecución paralela que muchos desarrolladores optaban por cadenas secuenciales. Async/await hace que el paralelismo parezca secuencial, lo que puede llevar a los desarrolladores a escribir código más lento que su predecesor basado en callbacks.

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 rechaza si alguna Promesa en el array se rechaza. Cuando las operaciones independientes pueden fallar sin afectarse entre sí y el llamador necesita saber sobre todos los fallos, Promise.allSettled es la herramienta correcta:

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

Este patrón no tiene un equivalente natural en el código basado en devoluciones de llamada sin un contador manual o una biblioteca como async.parallelMigrando a Promise.allSettled Suele ser uno de los cambios con mayor impacto en una base de código asíncrona heredada.

Cómo evitar el antipatrón de espera en bucle

El error de ejecución secuencial en lugar de paralela aparece con mayor frecuencia dentro de los bucles:

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

El patrón await-in-loop es una de las regresiones más comunes que se introducen durante la migración a async/await del código basado en callbacks, porque los callbacks obligaban a los desarrolladores a pensar en la concurrencia de forma explícita, mientras que async/await la oculta.

Manejo de errores en Async/Await: Reemplazando la propagación de errores de devolución de llamada

Las funciones de devolución de llamada propagan los errores por convención: el primer argumento de cada función es un error o nulo. Esto funciona, pero requiere que cada llamador verifique manualmente el argumento de error. Async/await propaga los errores a través del mecanismo de rechazo de promesas, que se integra con el bloque try/catch nativo de 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;
}

Un aspecto fundamental durante la migración es preservar el contexto de los errores. El código basado en devoluciones de llamada suele transmitir errores que se han complementado con información contextual en cada nivel. Al migrar a async/await, asegúrese de que el encapsulamiento de errores preserve este contexto:

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

Rechazos de promesas no gestionados

El código basado en devoluciones de llamada ignora silenciosamente los errores cuando se pasa por alto el argumento de error. Async/await produce rechazos de promesas no gestionados, que en Node.js 15+ provocan la terminación del proceso por defecto. Esto supone un cambio importante al migrar: el código que antes fallaba silenciosamente ahora se bloqueará.

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

Audite todas las llamadas a funciones asíncronas de tipo "disparar y olvidar" durante la migración. Cualquier llamada a una función asíncrona cuyo valor de retorno no se espere o se encadene con .catch() Es un posible fallo silencioso en el mundo de las devoluciones de llamada, pero un fallo por rechazo no controlado en el mundo de async/await.

Migración de patrones EventEmitter a promesas e iteradores asíncronos

Los EventEmitters de Node.js son un patrón de devolución de llamada en el que se registran múltiples devoluciones de llamada para eventos con nombre. Son comunes en flujos, conexiones de red y buses de eventos personalizados. La migración directa a async/await requiere encapsular la API basada en eventos.

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

Convertir un evento único en una Promesa es sencillo:

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

Para flujos que emiten múltiples eventos de datos, Node.js proporciona events.on que devuelve un iterador asíncrono, lo que permite consumir el flujo completo con for await...of:

javascript

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

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

Los flujos legibles de Node.js también son directamente iterables como iterables asíncronos desde 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;
}

Patrones de migración Async/Await en TypeScript

Los códigos base de TypeScript tienen consideraciones adicionales al migrar a async/await. Los tipos de retorno deben actualizarse de firmas de devolución de llamada a Promise<T>y el compilador garantiza que await solo se utilice dentro de funciones asíncronas.

mecanografiado

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

Las comprobaciones estrictas de nulidad de TypeScript interactúan con el código asíncrono de una manera que detecta errores comunes de migración. Si una función previamente devolvió User | null a través de una devolución de llamada y la migración lo cambia a Promise<User> (lanzando en lugar de devolver null), TypeScript detectará a los llamadores que comprobaron si era null pero ya no necesitan hacerlo, y a los llamadores que no comprueban los errores que ahora deben manejar.

Para el código TypeScript heredado que utiliza el @types/node firmas de devolución de llamada, util.promisify Está completamente tipado e infiere automáticamente el tipo de retorno Promise correcto para las funciones integradas de Node.js.

Estrategia de migración incremental para sistemas de producción

Un sistema de producción no se puede migrar de una sola vez. El enfoque incremental convierte un módulo a la vez, lo valida y luego pasa al siguiente. La clave es mantener una clara distinción entre el código convertido y el no convertido en cada etapa.

El orden de migración debe seguir la dirección de las dependencias: primero se convierten las dependencias más profundas y luego se avanza hacia las funciones que las llaman. Esto garantiza que, cuando se convierte una función de nivel superior, sus dependencias ya devuelven Promises y la capa de envoltura ya no es necesaria.

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

Este enfoque por etapas está directamente respaldado por herramientas de análisis. Como se discute en análisis de datos y flujo de controlPara una refactorización incremental segura, es fundamental comprender cómo fluyen los datos a través de las capas asíncronas antes de modificarlas. La dirección de las dependencias determina qué módulos se pueden convertir primero y cuáles deben esperar hasta que sus dependencias se hayan migrado.

Capas de envoltura para compatibilidad con versiones anteriores

Durante la migración, algunos usuarios seguirán esperando API basadas en devoluciones de llamada. util.callbackify la función es la inversa de util.promisify: convierte una función asíncrona de nuevo en una interfaz de devolución de llamada con manejo de errores primero:

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

Esta compatibilidad bidireccional significa que la conversión no es un evento puntual. Los módulos individuales se pueden convertir en cualquier sprint sin necesidad de coordinar con todos los solicitantes simultáneamente.

Errores comunes relacionados con Async/Await durante la migración

Falta el uso de await en las llamadas a funciones asíncronas.

El error de migración más común es llamar a una función asíncrona sin esperar a que finalice. Esto pasa desapercibido para el entorno de ejecución de JavaScript, a menos que la función la rechace, y en ese caso, el rechazo se convierte en un rechazo de promesa no gestionado en lugar de un error.

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 y ESLint no-floating-promises Esta regla detecta automáticamente este patrón. Se recomienda encarecidamente añadir esta regla de análisis estático durante la migración.

Funciones asíncronas en métodos de array

Array.prototype.forEach no espera a las devoluciones de llamada asíncronas. Esto produce el mismo comportamiento secuencial pero incorrecto que await-in-loop, excepto que el código parece funcionar mientras ejecuta silenciosamente todas las devoluciones de llamada concurrentemente sin esperar a ninguna:

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 no captura errores asíncronos fuera de Await.

Un bloque try/catch solo captura errores de expresiones con await. Una llamada a una función asíncrona sin await no tendrá su rechazo capturado por un bloque try/catch circundante:

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

Pruebas de código asíncrono después de la migración

Las funciones asíncronas migradas requieren casos de prueba asíncronos. Los marcos de prueba modernos lo admiten de forma nativa.

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

Al validar que una función migrada produce una salida idéntica a la de su predecesora de devolución de llamada, las pruebas de comparación son efectivas:

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

Este patrón de prueba es particularmente útil durante el período de transición cuando ambas versiones se ejecutan en paralelo. Como se examinó en el contexto de análisis de impacto antes de realizar cambios en el códigoValidar que una función refactorizada produce resultados idénticos a los de su predecesora es la confirmación basada en evidencia de que la migración es correcta antes de eliminar la versión anterior.

Cómo SMART TS XL Admite la migración asíncrona segura a gran escala.

En el caso de bases de código empresariales donde las cadenas de devolución de llamada abarcan varios archivos, servicios y equipos, el primer requisito para una migración segura es un mapa completo de las dependencias asíncronas existentes: qué funciones llaman a cuáles, qué datos se transmiten entre ellas, qué cadenas son rutas críticas para las operaciones principales de la aplicación y cuáles son utilidades aisladas que se pueden migrar de forma independiente.

SMART TS XL Este mapa de dependencias se construye analizando todo el código fuente, no solo archivos individuales. Determina cómo las funciones basadas en devoluciones de llamada se referencian entre sí a través de los límites de los módulos, identifica qué estado compartido se transmite mediante cierres y visualiza las cadenas de ejecución que deben permanecer intactas durante la migración. Este análisis estructural proporciona la información necesaria para el enfoque de migración por etapas descrito en esta guía: qué módulos se encuentran en la parte inferior del gráfico de dependencias y pueden convertirse primero, y qué módulos deben esperar hasta que sus dependencias ya se hayan migrado.

La plataforma de análisis de impacto La capacidad extiende esto a la evaluación de cambios. Antes de convertir un módulo basado en devolución de llamada para que devuelva Promises, el análisis de impacto identifica todos los demás módulos en el código base que lo llaman con una interfaz de devolución de llamada. Estos llamadores son el alcance de la migración para la siguiente etapa: deben convertirse simultáneamente o mediante un envoltorio compatible con versiones anteriores. util.callbackify Deben mantenerse hasta que se conviertan. Sin esta enumeración, las migraciones se desarrollan con un alcance desconocido y producen fallos inesperados cuando los llamadores no identificados encuentran una Promesa en lugar de una devolución de llamada.

Para bases de código que mezclan JavaScript con TypeScript, o que llaman a servicios de backend en otros lenguajes, SMART TS XL, análisis de dependencias entre idiomas proporciona visibilidad de toda la ruta de ejecución, no solo de la capa de JavaScript. Una cadena de devolución de llamada que termina en una llamada a un servicio externo escrito en Java o Python tiene dependencias que las herramientas de un solo lenguaje no pueden ver, y la planificación de la migración que ignora esas dependencias es incompleta. visualización de dependencias que SMART TS XL Esta función permite visualizar esas relaciones transfronterizas antes de que se realicen cambios en la migración.

Mantener la migración: De Callback a Async/Await en todo el código fuente

La transición de callbacks a async/await no se completa cuando se convierte el primer módulo. Se completa cuando se elimina la última capa de envoltura y el código base no tiene código restante. callback La convención en su lógica central. Lograrlo requiere disciplina durante todo el período de migración: el código nuevo debe escribirse con async/await, las capas de envoltura deben tratarse como temporales y las reglas de ESLint deben garantizar que no se introduzcan funciones de tipo callback en los módulos convertidos.

Los indicadores prácticos de una migración completada son: no util.promisify llamadas en el código de la aplicación (solo fueron necesarias durante el período de transición), no (err, result) => patrones en la lógica empresarial central (han sido reemplazados por try/catch en funciones asíncronas), no hay constructores Promise manuales donde async/await sería suficiente, y Promise.all en todos los lugares donde anteriormente se realizaban operaciones independientes de forma secuencial.

Cada uno de estos aspectos se puede medir mediante análisis estático, lo que permite realizar un seguimiento y reportar el progreso de forma objetiva, en lugar de estimarlo. Para los equipos que operan a gran escala, la combinación del análisis estático automatizado para identificar patrones de devolución de llamada y el análisis de dependencias para definir el alcance de cada etapa de la migración es lo que marca la diferencia entre una migración que se completa en un plazo determinado y una que se prolonga indefinidamente porque su alcance nunca se conoció por completo.