Módulo 6: Depurar, endurecer y decidir: el nodo Code en producción

3. Leer errores y lanzar los tuyos

Descripción

Al terminar esta lección vas a poder leer un mensaje de error de n8n y decir, en segundos, qué tipo de fallo es y dónde buscarlo. Vas a reconocer los errores del nodo Code que la documentación oficial cataloga, y los tres errores de JavaScript que causan la mayoría de las caídas reales. Vas a saber lanzar tus propios errores con throw new Error() y —más importante— escribir mensajes que le sirvan a quien los reciba a las tres de la mañana. Vas a manejar las tres opciones de la configuración On Error y entender qué hace cada una con el flujo. Vas a conocer el workflow de error con su Error Trigger y la forma exacta de su payload. Y vas a aprender el patrón que sostiene todo el endurecimiento del módulo: apartar el registro malo en un item de error sin detener el lote.

Esto importa porque un error es información, y casi todo el mundo la desperdicia dos veces. La primera, al leerlo: se ve un bloque rojo, se copia la primera línea a un buscador y se pega la solución de alguien que tenía otro problema. La segunda, al lanzarlo: se escribe throw new Error('Error en el pedido'), que a las tres de la mañana equivale a no haber escrito nada. Un mensaje de error es el único canal de comunicación que tienes con tu yo futuro y con quien te sustituya. Escribirlo bien cuesta treinta segundos y ahorra horas.

Conexión con el módulo: la lección 2 te dio el instrumental para encontrar la ejecución que falló; esta te enseña a leer lo que esa ejecución te dice y a decidir qué debe pasar cuando algo va mal. Es la lección que cierra la primera de las cinco preguntas de operación —¿qué pasa ante un timeout?—, porque un timeout es un error como cualquier otro y lo que importa es cómo se propaga. La lección 4 usa el patrón del item de error para todas sus guardas; la 5 lo extiende a la idempotencia; y el proyecto final exige que el workflow propague un item de error útil, que es literalmente lo que construyes aquí.

Anatomía de un error

Un error es un parte, no un grito. Tiene partes, y cada parte responde una pregunta distinta.

Piensa en la diferencia entre que alguien te diga "me duele" y que te entregue un parte médico. Lo primero te dice que hay un problema. Lo segundo te dice qué tipo de problema, dónde, con qué intensidad y desde cuándo. Un error de programación es siempre lo segundo, aunque la primera impresión sea de grito.

Cuando un nodo Code falla, n8n te muestra algo con esta estructura:

ERROR: Cannot read properties of undefined (reading 'quantity')  [line 14]

TypeError: Cannot read properties of undefined (reading 'quantity')
    at VmCodeWrapper (evalmachine.<anonymous>:14:31)
    ...

Desármalo por piezas.

El tipo de error. TypeError. Esta palabra es la más informativa de todo el bloque y casi nadie la mira. JavaScript tiene un conjunto pequeño de tipos de error, y cada uno significa una familia distinta de problema:

TipoQué significaDónde buscar
TypeErrorIntentaste hacer algo con un valor que no lo permite. La causa número uno: el valor era undefined o nullLa línea que indica; el dato que llegó
ReferenceErrorUsaste un nombre que no existe. Una variable mal escrita, o algo que creías disponible y no lo estáOrtografía; el Módulo 3 (variables que no existen en el nodo Code)
SyntaxErrorEl código no es JavaScript válido. Un paréntesis sin cerrar, una coma de másEl editor suele subrayarlo antes de ejecutar
RangeErrorUn valor está fuera del rango permitido. Poco frecuente en el nodo CodeBucles infinitos, arreglos gigantes

El mensaje. Cannot read properties of undefined (reading 'quantity'). Léelo al revés y es casi una frase en español: "intenté leer la propiedad quantity de algo, y ese algo era undefined".

Esto es enormemente específico. No dice "algo falló"; dice qué propiedad intentaste leer. Si tu código tiene line.quantity, entonces line era undefined. Eso reduce el problema a una pregunta concreta: ¿por qué line no tenía valor?

El número de línea. [line 14]. Te dice exactamente dónde. Con una advertencia que ahorra desconcierto: la numeración puede estar desplazada respecto de lo que ves en el editor, porque n8n envuelve tu código antes de ejecutarlo. Si la línea 14 de tu editor no tiene nada que ver, revisa las líneas de alrededor.

El rastro. Las líneas que empiezan con at. En un nodo Code casi nunca aportan, porque tu código es un solo bloque; en programas grandes indican la cadena de llamadas. Puedes ignorarlas la mayor parte del tiempo.

Y el dato que el bloque de error no te da: el nodo. En n8n lo aporta la interfaz. El nodo que falló queda marcado en el lienzo, y al abrir la ejecución puedes entrar a él directamente. Ese es el punto de partida de la lección 2: mira su panel de entrada antes que su código.

La lectura completa de nuestro ejemplo, en una frase: "en la línea 14, intenté leer quantity de una variable llamada line que no tenía valor, dentro del nodo X." De ahí a la causa hay un paso: el arreglo recorrido estaba vacío, o tenía un hueco, o no era un arreglo.

Los errores del nodo Code que la documentación cataloga

n8n mantiene una página de problemas comunes específica del nodo Code. Vale la pena conocerla entera, porque son exactamente los errores que vas a encontrar y sus mensajes son reconocibles.

Uno: el código no devuelve items en el formato correcto. El más frecuente de todos y el que trabajaste a fondo en el Módulo 1. La recomendación oficial: "asegúrate de que los datos se devuelvan como un arreglo de objetos con una clave json". Si tu return entrega un objeto suelto, un número o un texto, es esto.

Dos: una propiedad json no es un objeto. Ocurre cuando la clave json apunta a algo que no es un objeto —típicamente un arreglo—. La corrección: "asegúrate de que la clave json referencia un objeto en los datos que devuelves".

// ❌ json apunta a un arreglo
return [{ json: [1, 2, 3] }];

// ✅ json apunta a un objeto que contiene el arreglo
return [{ json: { values: [1, 2, 3] } }];

Tres: el código no devuelve un objeto. El caso de un script que no devuelve nada —olvidaste el return— o devuelve algo inesperado. La documentación añade una recomendación que vale para depurar en general: verifica que los datos que referencias existen y tienen la estructura esperada.

Cuatro: 'import' and 'export' may only appear at the top level. Este merece explicación. Las instrucciones import y export son la forma moderna de traer código de otros archivos en JavaScript, y no están soportadas en el entorno del nodo Code. La documentación indica cambiar los import por require.

Y aquí conviene juntar esto con lo que ya sabes: en n8n Cloud solo tienes disponibles el módulo crypto de Node.js y el paquete moment. Así que la corrección real casi nunca es "cambia import por require" sino "ese código no es para el nodo Code". Si lo copiaste de un ejemplo de Node.js, estás importando algo que no existe aquí.

Cinco: Cannot find module. Pediste un módulo que no está disponible. La documentación aclara que la solución solo aplica a instalaciones autoalojadas: instalar el módulo en el entorno de n8n y habilitar las variables NODE_FUNCTION_ALLOW_BUILTIN y NODE_FUNCTION_ALLOW_EXTERNAL. En Cloud no hay nada que hacer: no puedes agregar módulos.

Seis: no se puede acceder a credenciales desde un nodo Code. Textualmente: "métodos como this.getCredentials() o $getCredentials() van a producir errores". Es la misma frontera del Módulo 5: el nodo Code no llama a APIs y no ve credenciales. La recomendación oficial es usar el nodo HTTP Request para llamadas autenticadas.

Este último error es especialmente valioso de reconocer porque su causa suele ser un ejemplo copiado de la documentación de nodos personalizados, que es un producto distinto. Si ves this.helpers o this.getCredentials en un fragmento que alguien te pasó para un nodo Code, ya sabes que ese fragmento no es para aquí.

Los tres errores de JavaScript que más vas a ver

Más allá del catálogo de n8n, hay tres patrones que causan la mayoría de las caídas reales.

Cannot read properties of undefined (reading 'X')

El campeón indiscutible. Significa que intentaste acceder a una propiedad de algo que no existe.

const order = $input.first().json;

// Si el pedido vino sin line_items:
const firstLine = order.line_items[0];        // ← ✗ TypeError aquí
const quantity = firstLine.quantity;

Cómo se produce en Cumbre: el canal whatsapp manda formularios donde los campos opcionales sencillamente no vienen. order.line_items es undefined, y undefined[0] revienta.

Un matiz que confunde: el mensaje nombra la propiedad que intentaste leer, no la que faltaba. Cannot read properties of undefined (reading '0') significa que el undefined era line_items, no [0]. Lee el mensaje así: "lo que está roto es lo que va a la izquierda del punto".

X is not a function

Llamaste algo como si fuera una función y no lo es.

const name = order.customer_name;
const clean = name.trim();       // ← ✗ si customer_name es un número: name.trim is not a function

Cómo se produce en Cumbre: el canal rep_csv a veces manda un identificador numérico donde se esperaba texto. Los números no tienen .trim().

Una variante frecuente: order.line_items.map is not a function, que significa que line_items existe pero no es un arreglo —es un texto, o un objeto—. Es exactamente el caso que Array.isArray() detecta.

X is not iterable

Usaste for...of sobre algo que no se puede recorrer.

for (const line of order.line_items) {   // ← ✗ si line_items es undefined o un número
  // ...
}

Los arreglos y los textos son iterables. undefined, null, los números y los objetos planos no lo son. Este es el error que tumba la versión ingenua del nodo Enrich orders de la lección 1.

El patrón de fondo, y la razón de que la lección 4 exista: los tres errores tienen la misma raíz. Diste por hecho la forma de un dato que llegó de afuera. No son errores de lógica: son supuestos no verificados.

Lanzar tus propios errores

Hasta aquí hablamos de errores que te pasan. Ahora los que tú provocas a propósito, que es una herramienta y no un fracaso.

Qué es throw

throw es una instrucción de JavaScript que significa "detente aquí y reporta este problema". Se usa con un objeto Error, que es simplemente un contenedor con un mensaje:

throw new Error('El pedido llegó sin order_id');

Cuando esa línea se ejecuta, el script se detiene inmediatamente —lo que venga después no corre— y n8n marca el nodo como fallido, mostrando tu mensaje en el panel de error.

Por qué querrías detener algo a propósito. Porque hay situaciones donde continuar es peor que parar. Si tu script está a punto de escribir en el CRM un pedido con order_total: NaN, detenerse y avisar es mucho mejor que escribir basura y que alguien la descubra en tres semanas.

Es el mismo razonamiento del sensor de una máquina industrial: la máquina no se detiene porque esté rota, se detiene para no romperse. Un throw bien puesto es un sensor.

Cómo escribir un mensaje de error útil

Aquí está la parte que se paga, y es sorprendentemente rara.

Un mensaje de error tiene un lector concreto: alguien —quizás tú— que va a leerlo sin contexto, posiblemente de madrugada, posiblemente sin haber escrito ese código. Un mensaje útil responde cuatro preguntas:

PreguntaQué poner
¿Qué falló?La condición concreta, no "hubo un error"
¿Dónde?El nodo, el item, el índice; algo que permita localizarlo
¿Con qué dato?El valor real que provocó el problema
¿Qué hago?La acción sugerida, si la hay

Compara:

// ❌ Inútil. Dice que algo pasó, no qué ni dónde.
throw new Error('Error en el pedido');

// ❌ Un poco mejor, pero sigue mandando a leer el código.
throw new Error('Falta un campo requerido');

// ✅ Responde las cuatro preguntas.
throw new Error(
  `El pedido ${order.order_id} (item ${i}, canal ${order.channel}) no tiene line_items. ` +
  `Recibido: ${JSON.stringify(order.line_items)}. ` +
  `Revisa el mapeo del formulario de WhatsApp, que es el canal que omite campos opcionales.`
);

Ese tercer mensaje se lee así en el panel de error:

El pedido ORD-2045 (item 4, canal whatsapp) no tiene line_items.
Recibido: undefined. Revisa el mapeo del formulario de WhatsApp,
que es el canal que omite campos opcionales.

Alguien que nunca vio el código puede actuar con eso. Ese es el estándar.

Tres reglas prácticas para escribirlos:

Incluye siempre el identificador del registro. El pedido ORD-2045... te deja ir directo a ese pedido. Sin él, tienes veinte pedidos y ningún camino.

Incluye el valor real, no solo su ausencia. Recibido: undefined y Recibido: "12" llevan a diagnósticos distintos. JSON.stringify() es tu amigo aquí porque muestra las comillas: distingue el número 12 del texto "12".

No metas secretos en el mensaje. Un mensaje de error queda guardado en el historial de ejecuciones y puede terminar en un canal de alertas. Nunca incluyas tokens, contraseñas ni datos personales sensibles. Vale la pena decirlo porque el impulso natural al depurar es volcarlo todo.

El nodo Stop and Error

Existe también un nodo visual dedicado a lo mismo: Stop and Error. Sirve para forzar que una ejecución falle bajo la condición que tú decidas, sin escribir código.

Su parámetro principal se llama Error Type y tiene dos modos:

  • Error Message — un campo donde escribes el mensaje a lanzar.
  • Error Object — un campo donde pones un objeto JSON con las propiedades del error.

Cuándo usar el nodo y cuándo el throw. El nodo es preferible cuando la condición de fallo es simple y quieres que sea visible en el lienzo: alguien que abre el workflow ve una rama que termina en un Stop and Error y entiende la regla sin leer código. El throw es preferible cuando la condición depende de un cálculo que ya estás haciendo dentro del script, porque sacarla a un nodo aparte duplicaría la lógica.

Es el mismo criterio del Módulo 4 aplicado al manejo de errores: si cabe en el lienzo y se lee mejor ahí, va en el lienzo.

Cómo se propaga un error por el workflow

Lanzar un error es la mitad. La otra mitad es qué pasa después, y eso lo decide la configuración del nodo, no tu código.

La configuración On Error

Todo nodo de n8n tiene una pestaña de ajustes con una opción llamada On Error, con tres valores:

ValorQué hace
Stop WorkflowDetiene la ejecución completa. Es el comportamiento por defecto
ContinueEl workflow sigue adelante a pesar del error, usando los últimos datos válidos
Continue (using error output)El workflow sigue, y la información del error sale por una salida separada del nodo

Las tres merecen comentario.

Stop Workflow es el defecto y suele ser lo correcto. Si algo salió mal y no sabes qué hacer al respecto, detenerse es honesto: no produce datos falsos y deja una ejecución fallida visible en la lista.

Continue es la opción peligrosa. El workflow sigue como si nada, con los últimos datos válidos. En algunos casos es exactamente lo que quieres —un nodo opcional de enriquecimiento que falla y no debe frenar el proceso—. En muchos otros produce el peor de los mundos: un workflow en verde que procesó datos incompletos y nadie se enteró. Si activas esta opción, haz que quede constancia en los datos.

Nota de vocabulario: en material anterior vas a ver esta capacidad llamada continueOnFail. Es el nombre antiguo del mismo parámetro. Si un tutorial habla de continueOnFail, se refiere a lo que hoy es On Error.

Continue (using error output) es la más útil de las tres y la menos conocida. El nodo pasa a tener dos salidas: por la principal salen los items que se procesaron bien, y por la de error sale la información de lo que falló. Eso te permite construir esto:

                          ┌─► (salida normal) ─► [Escribir en el CRM]
[Code: Validate orders] ──┤
                          └─► (salida de error) ─► [Slack: avisar al equipo]

Un solo nodo, dos caminos, ninguna decisión perdida. Los pedidos buenos siguen; los malos van a un canal donde una persona los mira.

Las otras opciones de la pestaña de ajustes

Junto a On Error viven tres opciones más que cambian el comportamiento ante fallos y conviene conocer.

Retry On Fail — vuelve a ejecutar el nodo automáticamente si falla. Es la respuesta correcta a errores transitorios: una API que devolvió un 503, una red que parpadeó. Y es la respuesta equivocada a errores de datos: si line_items no existe, no va a existir en el segundo intento tampoco; solo vas a fallar tres veces más despacio.

⚠️ Reintentar vuelve a ejecutar el efecto. Si el nodo escribe en un sistema externo y falla después de haber escrito, el reintento escribe otra vez. Es el mismo argumento del botón de reintento de la lección 2, y la razón de que la lección 5 exista.

Always Output Data — hace que el nodo devuelva un item vacío incluso cuando no produce datos. Sirve para que el workflow no se detenga en seco cuando un filtro no deja pasar nada, y para tener una señal visible de que el nodo corrió.

Execute Once — hace que el nodo procese solo el primer item que recibe. No es una opción de errores, pero conviene reconocerla porque produce un síntoma desconcertante: "mi nodo solo procesa un pedido y no sé por qué".

El workflow de error

Hay un último nivel, y es el que convierte un fallo en una alerta.

En los ajustes de cualquier workflow puedes designar un workflow de error: otro workflow que se ejecuta automáticamente cuando este falla. La documentación lo describe así: "puedes configurar un workflow de error en la configuración del workflow. Se ejecuta si una ejecución falla".

Ese workflow de error debe empezar con el nodo Error Trigger. Y ese nodo recibe un payload con una forma documentada que conviene conocer, porque es con lo que vas a armar tu alerta:

{
  "execution": {
    "id": "231",
    "url": "https://n8n.example.com/execution/231",
    "retryOf": "34",
    "error": {
      "message": "Example Error Message",
      "stack": "Stacktrace"
    },
    "lastNodeExecuted": "Node With Error",
    "mode": "manual"
  },
  "workflow": {
    "id": "1",
    "name": "Example Workflow"
  }
}

Mira lo que hay ahí, porque es oro: el mensaje de tu error —el que escribiste con throw—, la URL directa a la ejecución que falló, el nombre del último nodo que se ejecutó, y el nombre del workflow. Con eso puedes mandar un mensaje a un canal de Slack que diga exactamente qué falló, en qué workflow, y con un enlace para ir a verlo.

Y ahora se entiende del todo por qué el mensaje del throw importa tanto: ese texto es lo que aparece en la alerta. Si escribiste 'Error en el pedido', tu alerta dice "Error en el pedido". Si escribiste el mensaje de las cuatro preguntas, tu alerta dice qué pedido, de qué canal, con qué valor y qué revisar.

Dos límites documentados que hay que conocer. El primero: "no puedes probar los workflows de error ejecutando workflows manualmente. El Error Trigger solo se dispara cuando falla un workflow automático." Es decir, para probarlo tienes que publicar el workflow y dispararlo con su trigger real. El segundo: si el error ocurre en el propio nodo trigger, la estructura del payload es distinta y no incluye execution.id ni execution.url, porque el workflow nunca llegó a ejecutarse.

También existe una alternativa para forzar el disparo del workflow de error a voluntad: el nodo Stop and Error que viste antes, que "fuerza a las ejecuciones a fallar bajo las circunstancias que elijas, y dispara el workflow de error".

Ejemplo trabajado: tres estrategias para el mismo problema

El requerimiento es el de siempre, con una regla nueva:

"Valida los pedidos de Cumbre antes de mandarlos al CRM. Un pedido es válido si tiene order_id, tiene al menos una línea, y todas sus líneas tienen quantity y unit_price numéricos."

Vamos a resolverlo tres veces, con tres estrategias distintas, y a comparar cuándo conviene cada una.

Estrategia A: fallar duro con throw

// ============================================================
// Nodo: Code — "Validate orders (strict)"
// Modo: Run Once for All Items
// ESTRATEGIA: si algo está mal, se detiene TODO el lote.
// ============================================================

const items = $input.all();
const output = [];

for (let i = 0; i < items.length; i++) {
  const order = items[i].json;

  if (!order.order_id) {
    throw new Error(
      `Item ${i} llegó sin order_id. Canal: ${order.channel ?? 'desconocido'}. ` +
      `Sin identificador no se puede procesar ni rastrear el pedido.`
    );
  }

  const lines = order.line_items;
  if (!Array.isArray(lines) || lines.length === 0) {
    throw new Error(
      `El pedido ${order.order_id} (item ${i}, canal ${order.channel}) no tiene líneas válidas. ` +
      `Recibido en line_items: ${JSON.stringify(lines)}. ` +
      `Revisa el mapeo del canal de origen.`
    );
  }

  for (let j = 0; j < lines.length; j++) {
    const quantity = Number(lines[j]?.quantity);
    const unitPrice = Number(lines[j]?.unit_price);

    if (!Number.isFinite(quantity) || !Number.isFinite(unitPrice)) {
      throw new Error(
        `El pedido ${order.order_id}, línea ${j} (SKU ${lines[j]?.sku ?? 'sin sku'}), ` +
        `tiene valores no numéricos. quantity=${JSON.stringify(lines[j]?.quantity)}, ` +
        `unit_price=${JSON.stringify(lines[j]?.unit_price)}. ` +
        `El canal rep_csv suele mandar números con separador de miles.`
      );
    }
  }

  output.push({ json: order, pairedItem: i });
}

return output;

Qué esperar con la semilla de cinco pedidos. El nodo falla. El mensaje que ves en el panel de error es:

El pedido ORD-2045 (item 4, canal whatsapp) no tiene líneas válidas.
Recibido en line_items: []. Revisa el mapeo del canal de origen.

Los cuatro pedidos anteriores no salen, aunque estaban perfectos. Eso es lo que significa "fallar duro".

Cuándo conviene: cuando el lote es una unidad indivisible. Si estás procesando las líneas de una sola factura, procesar la mitad es peor que no procesar nada. También cuando el fallo indica un problema sistémico —si un pedido no tiene order_id, probablemente el mapeo está roto y los demás también están mal—.

Estrategia B: apartar el registro malo en un item de error

// ============================================================
// Nodo: Code — "Validate orders (tolerant)"
// Modo: Run Once for All Items
// ESTRATEGIA: los pedidos válidos siguen; los inválidos salen
//             como items de error, marcados y con el motivo.
// SALIDA: mezcla de items con record_type 'order' y 'error'.
//         Usa un nodo Switch o Filter después para separarlos.
// ============================================================

const items = $input.all();
const output = [];

// Función que valida un pedido y devuelve el motivo del rechazo, o null si está bien
function validateOrder(order, index) {
  if (!order.order_id) {
    return `Item ${index} llegó sin order_id (canal ${order.channel ?? 'desconocido'}).`;
  }
  const lines = order.line_items;
  if (!Array.isArray(lines) || lines.length === 0) {
    return `Sin líneas válidas. line_items recibido: ${JSON.stringify(lines)}.`;
  }
  for (let j = 0; j < lines.length; j++) {
    const quantity = Number(lines[j]?.quantity);
    const unitPrice = Number(lines[j]?.unit_price);
    if (!Number.isFinite(quantity) || !Number.isFinite(unitPrice)) {
      return `Línea ${j} (SKU ${lines[j]?.sku ?? 'sin sku'}) con valores no numéricos: ` +
             `quantity=${JSON.stringify(lines[j]?.quantity)}, ` +
             `unit_price=${JSON.stringify(lines[j]?.unit_price)}.`;
    }
  }
  return null;   // sin problemas
}

for (let i = 0; i < items.length; i++) {
  const order = items[i].json;
  const problem = validateOrder(order, i);

  if (problem === null) {
    output.push({
      json: { record_type: 'order', ...order },
      pairedItem: i,
    });
  } else {
    output.push({
      json: {
        record_type: 'error',
        order_id: order.order_id ?? null,
        channel: order.channel ?? null,
        error_stage: 'validation',
        error_message: problem,
        original_index: i,
        // El pedido original, para poder reprocesarlo después de arreglarlo
        original_payload: order,
      },
      pairedItem: i,
    });
  }
}

return output;

Qué esperar con la semilla de cinco pedidos. El nodo no falla: sale en verde con 5 items. Cuatro con record_type: 'order'ORD-2041, ORD-2042, ORD-2043 y ORD-2044— y uno con record_type: 'error', que es ORD-2045, con este error_message:

Sin líneas válidas. line_items recibido: [].

Después de este nodo pones un Switch o un Filter que separe por record_type: los order van al CRM, los error van a una hoja de revisión o a un canal de Slack.

Cuándo conviene: casi siempre en procesamiento por lotes. Es la estrategia que hace posible que diecinueve pedidos buenos no se pierdan por culpa de uno malo, y la que sostiene el resto de este módulo.

El detalle que la hace valiosa: el campo original_payload. Guardar el pedido completo tal como llegó significa que cuando alguien arregle el mapeo del formulario, puedes reprocesar exactamente esos registros sin ir a buscarlos a la fuente.

Estrategia C: dejar que el nodo maneje el error

La tercera estrategia no está en el código: está en la configuración.

Escribes el código sin guardas, la versión ingenua, y en la pestaña de ajustes del nodo pones On Error en Continue (using error output). El nodo pasa a tener dos salidas, y cuando un item hace fallar el código, la información del error sale por la segunda.

Cuándo conviene: cuando los fallos son excepcionales y no quieres ensuciar el código con validación. Es cómodo y se ve limpio en el lienzo.

Cuándo no conviene, y es importante: cuando el nodo está en modo Run Once for All Items. En ese modo tu código corre una sola vez para todo el lote, así que un fallo tumba la corrida entera y la salida de error recibe el lote completo, no el item culpable. La granularidad por item que esta estrategia promete solo la obtienes de verdad en modo Run Once for Each Item. Verifícalo en tu instancia antes de apoyarte en ello.

La comparación

A: throwB: item de errorC: salida de error del nodo
¿Se pierden los items buenos?Sí, todosNoDepende del modo de ejecución
¿Dispara el workflow de error?NoNo
¿Queda visible en la lista de ejecuciones?Sí, en rojoNo, sale en verde
¿Cuánto código de más?PocoBastanteNinguno
¿Se puede reprocesar el registro malo?Hay que ir a la fuenteSí, original_payloadDepende

Y la combinación que uso yo, porque ninguna de las tres es suficiente sola:

Estrategia B para los datos, estrategia A para el sistema.

Un pedido con un campo mal formado es un problema de datos: se aparta como item de error y el lote sigue. Un nodo Config que no existe, un lote que llega vacío cuando siempre trae algo, o una configuración imposible es un problema del sistema: ahí sí throw, porque no hay ningún registro que apartar y continuar sería fingir que todo está bien.

// Problema del sistema → throw. No hay registro que apartar.
const configNode = $('Config');
if (!configNode) {
  throw new Error('El nodo "Config" no existe. Este script depende de él para los umbrales.');
}

// Problema de datos → item de error. El lote sigue.
if (!Array.isArray(order.line_items)) {
  output.push({ json: { record_type: 'error', /* ... */ } });
  continue;
}

Errores comunes

Escribir mensajes de error que no dicen nada (conceptual). Qué pasa: el workflow falla a las 3:00, la alerta llega al canal de Slack, y dice "Error procesando datos". Alguien tiene que entrar a n8n, buscar la ejecución, abrir el nodo y leer el código para saber qué pasó. Por qué pasa: al escribir el throw tienes todo el contexto en la cabeza y el mensaje corto parece suficiente. Cómo detectarlo: lee tus mensajes de error imaginando que los recibes por Slack sin ningún otro contexto; si no puedes actuar con lo que dicen, están mal. Cómo corregirlo: las cuatro preguntas —qué falló, dónde, con qué dato, qué hacer—. Y recuerda que ese texto es exactamente lo que va a aparecer en la alerta del workflow de error.

Usar Continue sin dejar rastro (práctico). Qué pasa: se pone On Error: Continue en un nodo para que el workflow deje de fallar, y a partir de ahí todas las ejecuciones salen en verde. Meses después se descubre que un tercio de los registros se procesó a medias. Por qué pasa: Continue resuelve el síntoma visible —el rojo— sin resolver el problema. Cómo detectarlo: revisa qué nodos de tus workflows tienen On Error distinto del valor por defecto y pregúntate, para cada uno, cómo te enterarías si empezara a fallar siempre. Cómo corregirlo: si un nodo debe continuar ante fallos, que quede constancia en los datos —un campo enrichment_failed: true— o usa Continue (using error output) y manda la rama de error a algún lado. Un error que no deja rastro no está manejado: está escondido.

Reintentar un error de datos (práctico). Qué pasa: se activa Retry On Fail en un nodo Code que falla porque line_items no existe, y el nodo falla tres veces en vez de una. Por qué pasa: el reintento es la respuesta correcta a fallos transitorios y se generaliza sin pensar. Cómo detectarlo: pregúntate si el segundo intento tiene alguna razón para ir mejor que el primero. Si el problema está en el dato de entrada, la respuesta es no. Cómo corregirlo: reserva Retry On Fail para nodos que hablan con el mundo exterior —HTTP Request, bases de datos— y resuelve los errores de datos con guardas dentro del código.

Copiar código de nodos personalizados a un nodo Code (práctico). Qué pasa: aparece un error diciendo que no se puede acceder a credenciales, o que this.helpers no está definido. Por qué pasa: la documentación de desarrollo de nodos personalizados es un producto distinto del nodo Code, con otro entorno y otros métodos disponibles, y sus ejemplos circulan mezclados. Cómo detectarlo: si el fragmento usa this.helpers, this.getCredentials() o $getCredentials(), no es para el nodo Code. La documentación lo confirma: esos métodos producen errores aquí. Cómo corregirlo: el patrón del Módulo 5 —el nodo Code arma, el nodo HTTP Request llama con su credencial gestionada—.

Esperar que el workflow de error se dispare en pruebas (práctico). Qué pasa: se configura un workflow de error con su Error Trigger, se ejecuta el workflow principal desde el editor para probarlo, falla, y la alerta nunca llega. Se concluye que la configuración está mal. Por qué pasa: está documentado —"no puedes probar los workflows de error ejecutando workflows manualmente; el Error Trigger solo se dispara cuando falla un workflow automático"—. Cómo detectarlo: si estás pulsando el botón de ejecutar en el editor, estás en el caso que no dispara. Cómo corregirlo: publica el workflow y dispáralo con su trigger real. Y ten presente el otro límite documentado: si el fallo ocurre en el propio trigger, el payload llega sin execution.id ni execution.url.

Meter datos sensibles en el mensaje de error (conceptual). Qué pasa: para depurar rápido se escribe throw new Error('Falló con: ' + JSON.stringify(order)), y ese volcado —que incluye nombre, dirección y teléfono del cliente— queda guardado en el historial de ejecuciones y sale replicado en el canal de alertas. Por qué pasa: al depurar, volcarlo todo es lo más rápido. Cómo detectarlo: revisa si alguno de tus mensajes de error incluye el registro completo en vez de campos concretos. Cómo corregirlo: nombra los campos que necesitas —order_id, channel, el valor que falló— en lugar de volcar el objeto entero. Y nunca incluyas tokens ni contraseñas: un mensaje de error es un texto que viaja lejos.

Ejercicios

Ejercicio 1 — Diagnostica cinco errores. Para cada mensaje, di qué tipo de error es, qué significa exactamente, y cuál es la causa más probable en el contexto de Cumbre.

(a) TypeError: Cannot read properties of undefined (reading 'toUpperCase') (b) TypeError: order.line_items.map is not a function (c) ReferenceError: $env is not defined (d) A 'json' property isn't an object (e) TypeError: Cannot read properties of null (reading 'json')

Ver solución

(a) TypeError. Intentaste llamar .toUpperCase() sobre algo que es undefined. En Cumbre, casi con certeza order.customer_name en un pedido del canal whatsapp, donde los campos opcionales del formulario no vienen. La corrección: (order.customer_name ?? '').toUpperCase(), o una guarda antes.

(b) TypeError. line_items sí existe —si no existiera el mensaje diría properties of undefined—, pero no es un arreglo, así que no tiene .map(). En Cumbre, típico de un CSV mal parseado que dejó el campo como texto. La corrección: Array.isArray(order.line_items) antes de usarlo.

(c) ReferenceError. El nombre $env no existe en este entorno. Es el delta de n8n 2.0 del Módulo 3: el acceso a variables de entorno desde el nodo Code está bloqueado por defecto. Ojo con el matiz: la lección 7 del Módulo 3 explicaba que el síntoma habitual es que $env.ALGO devuelva undefined en silencio; ver un ReferenceError explícito es un caso distinto y, en realidad, más amable, porque falla ruidosamente. Sea cual sea el síntoma en tu versión, la corrección es la misma: los secretos van en credenciales, la configuración entra por un nodo.

(d) Un error del catálogo del nodo Code, no de JavaScript. La clave json apunta a algo que no es un objeto, típicamente un arreglo. La corrección documentada: asegúrate de que json referencia un objeto. return [{ json: [1,2,3] }] está mal; return [{ json: { values: [1,2,3] } }] está bien.

(e) TypeError, y la distinción importa. El mensaje dice null, no undefined. En el nodo Code, la causa más frecuente es $input.first() cuando no llegó ningún item: no hay un primer item, así que .json sobre eso falla. La corrección: verificar $input.all().length > 0 antes, o usar el patrón del lote vacío que viste en la lección 1.

Por qué funciona: fíjate en que la diferencia entre (a) y (b) está solo en el mensaje, y decide dos correcciones distintas. properties of undefined significa que el campo falta; X is not a function significa que el campo está pero tiene otro tipo. Leer esa diferencia te ahorra probar la corrección equivocada.

Ejercicio 2 — Reescribe tres mensajes de error. Estos tres throw están en un workflow de Cumbre en producción. Reescríbelos para que respondan las cuatro preguntas —qué falló, dónde, con qué dato, qué hacer— sabiendo que el texto va a llegar por Slack sin ningún otro contexto.

// (a)
if (!order.customer_id) {
  throw new Error('Falta el cliente');
}

// (b)
if (total > 100000) {
  throw new Error('Total muy alto');
}

// (c)
if (!config) {
  throw new Error('Error de configuración');
}
Ver solución

(a)

if (!order.customer_id) {
  throw new Error(
    `El pedido ${order.order_id ?? '(sin order_id)'} del canal ${order.channel ?? 'desconocido'} ` +
    `llegó sin customer_id. Recibido: ${JSON.stringify(order.customer_id)}. ` +
    `Sin cliente no se puede facturar; revisa el mapeo del canal de origen.`
  );
}

Nota el ?? '(sin order_id)': si el pedido viene tan roto que tampoco tiene identificador, el mensaje sigue siendo legible en vez de decir "El pedido undefined".

(b)

if (total > MAX_ORDER_TOTAL) {
  throw new Error(
    `El pedido ${order.order_id} suma ${total} ${order.currency ?? 'MXN'}, ` +
    `que supera el tope de seguridad de ${MAX_ORDER_TOTAL}. ` +
    `Tiene ${order.line_items.length} líneas. ` +
    `Puede ser un error de captura (canal ${order.channel}) o un pedido legítimo grande: ` +
    `verifícalo con comercial antes de reprocesarlo.`
  );
}

Este caso enseña algo extra. El mensaje original —'Total muy alto'— no solo era vago: escondía que el umbral era un número mágico dentro del código. Al escribir el mensaje bien aparece la necesidad de nombrarlo (MAX_ORDER_TOTAL), y al nombrarlo aparece la pregunta de si debería estar en el nodo Config. Escribir buenos mensajes de error mejora el código, no solo la comunicación.

(c)

if (!config) {
  throw new Error(
    `El nodo "Config" no devolvió datos. Este script depende de él para los umbrales ` +
    `de envío gratis y de prioridad. Causas probables: el nodo fue renombrado, ` +
    `fue desconectado del flujo, o no se ejecutó antes que este. ` +
    `Workflow: ${$workflow.name}, ejecución: ${$execution.id}.`
  );
}

Aquí agregué el workflow y la ejecución. En un mensaje de datos suele ser redundante —el payload del Error Trigger ya los trae—, pero en un error de configuración es útil porque el mismo script puede estar copiado en varios workflows y la alerta te dice de inmediato cuál de ellos tiene el nodo renombrado.

El patrón común de los tres: cada mensaje nombra el registro o el recurso afectado, muestra el valor real que provocó el fallo, y termina con una acción sugerida. Ninguno de los tres obliga a abrir el código para entender qué pasó.

Ejercicio 3 — Diseña la estrategia de errores de un workflow. Cumbre tiene este workflow en producción, que corre cada hora:

Schedule Trigger
  └─► HTTP Request: "Fetch orders from store API"
        └─► Code: "Normalize and validate"
              └─► HTTP Request: "Push to CRM"
                    └─► Google Sheets: "Log processed orders"

Para cada uno de estos cuatro escenarios, decide: qué configuración On Error pondrías en qué nodo, si usarías throw o item de error dentro del código, y si el workflow de error debería dispararse. Justifica en dos frases.

(a) La API de la tienda devuelve un 503 porque está en mantenimiento. (b) Un pedido de los cuarenta que llegaron viene sin line_items. (c) El CRM rechaza un pedido concreto con un 422 porque el cliente no existe allí. (d) La credencial del CRM caducó y todas las llamadas devuelven 401.

Ver solución

(a) API en mantenimiento (503). Retry On Fail activado en el nodo Fetch orders, con On Error: Stop Workflow. Es un fallo transitorio y el reintento tiene una razón real para funcionar; si después de los reintentos sigue fallando, detenerse es correcto porque no hay datos con los que continuar. El workflow de error sí debe dispararse: alguien tiene que saber que la ingesta de esta hora no ocurrió. Y como el nodo solo lee, reintentar no tiene riesgo de duplicar nada.

(b) Un pedido sin line_items. Item de error dentro del código —estrategia B—, y On Error: Stop Workflow en el nodo Code, que aquí no se va a disparar porque el código no lanza. El workflow de error NO debe dispararse: treinta y nueve pedidos se procesaron bien y no hay ninguna emergencia. Después del nodo Code va un Switch por record_type: los pedidos van al CRM y los errores a una hoja de revisión que alguien mira una vez al día.

(c) El CRM rechaza un pedido con 422. On Error: Continue (using error output) en el nodo Push to CRM, con la rama de error yendo a la misma hoja de revisión que en (b). Es un fallo de ese registro, no del sistema: los otros treinta y nueve pedidos tienen que llegar al CRM. El workflow de error no debe dispararse por un rechazo individual. Nota que Retry On Fail sería un error aquí: un 422 significa que la petición está mal formada según el CRM, y reintentar exactamente la misma petición va a producir exactamente el mismo 422.

(d) Credencial caducada (401 en todo). Este es el caso interesante y es distinto de (c) aunque falle el mismo nodo. Un 401 sistemático no es un problema de datos: es un problema de sistema, y continuar significa mandar cuarenta pedidos a la rama de error uno por uno mientras el flujo sale "en verde" con una hoja llena de fallos. Aquí sí quieres detener y alertar.

La solución práctica: mantén Continue (using error output) para no perder el lote, y agrega después un nodo Code que cuente los errores y decida:

// Nodo: Code — "Assess CRM failures"
// Si falla casi todo, es un problema de sistema y hay que detenerse.
const failures = $input.all();
const total = $('Normalize and validate').all().length;

if (failures.length >= total * 0.5) {
  throw new Error(
    `${failures.length} de ${total} pedidos fallaron al escribir en el CRM. ` +
    `Esto no parece un problema de datos individuales sino del sistema. ` +
    `Primer error: ${failures[0]?.json?.error?.message ?? 'sin detalle'}. ` +
    `Revisa la credencial del CRM y el estado del servicio antes de reprocesar.`
  );
}

return failures;   // pocos fallos: siguen a la hoja de revisión

El principio que este ejercicio enseña, y que vale para cualquier workflow:

Un fallo aislado es un problema de datos: apártalo y sigue. Un fallo generalizado es un problema de sistema: detente y alerta.

La proporción es la señal. Y fíjate en que ese juicio no se puede hacer nodo por nodo: hace falta un nodo Code que mire el conjunto y decida. Es exactamente el tipo de lógica que no cabe en un formulario, y por lo tanto exactamente para lo que existe el nodo Code.

Resumen y siguiente paso

En esta lección aprendiste a leer y a producir errores.

Un error es un parte, no un grito: tiene un tipoTypeError, ReferenceError, SyntaxError, RangeError—, un mensaje, un número de línea (que puede estar desplazado, porque n8n envuelve tu código) y un rastro que en el nodo Code casi nunca aporta. El nodo lo aporta la interfaz.

Conociste el catálogo oficial de errores del nodo Code: no devolver items en el formato correcto, una clave json que no apunta a un objeto, no devolver nada, import/export no soportados —usa require, y en Cloud solo hay crypto y moment—, módulos no encontrados —solo resoluble en autoalojado con NODE_FUNCTION_ALLOW_BUILTIN y NODE_FUNCTION_ALLOW_EXTERNAL— y el intento de leer credenciales, que la documentación confirma que produce errores. Y los tres errores de JavaScript que más vas a ver: Cannot read properties of undefined, X is not a function y X is not iterable, con la misma raíz: diste por hecho la forma de un dato que llegó de afuera.

Aprendiste a lanzar los tuyos con throw new Error(), y sobre todo a escribir el mensaje: qué falló, dónde, con qué dato, qué hacer. Ese texto es exactamente lo que aparece en la alerta del workflow de error, así que no es cosmética. Viste el nodo Stop and Error con sus dos modos —Error Message y Error Object—, la opción On Error con sus tres valores —Stop Workflow, Continue, Continue (using error output)—, más Retry On Fail, Always Output Data y Execute Once. Y el workflow de error con su Error Trigger, su payload documentado —mensaje, URL de la ejecución, último nodo ejecutado, nombre del workflow— y sus dos límites: no se dispara en ejecuciones manuales, y si el fallo ocurre en el trigger el payload llega sin execution.id.

Y resolviste el mismo problema con tres estrategias, con la combinación que recomiendo: item de error para los problemas de datos, throw para los problemas de sistema.

Antes de avanzar deberías poder: distinguir de un vistazo Cannot read properties of undefined de X is not a function y decir qué corrección pide cada uno; escribir un mensaje de error que alguien pueda accionar recibiéndolo por Slack; y explicar por qué On Error: Continue sin dejar rastro es peor que dejar que el nodo falle.

La lección 4 se ocupa de la raíz común de los tres errores más frecuentes: los supuestos no verificados sobre los datos. Vas a aprender la diferencia exacta entre undefined, null, la cadena vacía, el cero y NaN —y por qué confundirlos produce los bugs más difíciles de encontrar—. Vas a manejar el encadenamiento opcional ?. y el operador ??, con la trampa clásica del || cuando el valor legítimo es 0. Vas a validar tipos antes de operar con typeof, Array.isArray() y Number.isFinite(). Y vas a construir la función normalizeOrder() de Cumbre: una pieza que toma un pedido de cualquiera de los tres canales y devuelve siempre la misma forma, que es la base del proyecto final.

Recursos