23 KiB
Error Patterns - JavaScript Code Node
Complete guide to avoiding the most common Code node errors.
Overview
This guide covers the top 5 error patterns encountered in n8n Code nodes. Understanding and avoiding these errors will save you significant debugging time.
Error frequency (roughly ordered):
- Empty Code / Missing Return - the dominant error n8n itself rejects
- Expression Syntax used as Code -
{{ }}written where JavaScript belongs - Return Shape - primitive/
nullreturns (bare objects auto-wrap) - Broken Strings / Escaping - unbalanced quotes/brackets throw a JS syntax error
- Missing Null Checks - common runtime error
Error #1: Empty Code or Missing Return Statement
Frequency: Most common error (38% of all validation failures)
What Happens:
- Workflow execution fails
- Next nodes receive no data
- Error: "Code cannot be empty" or "Code must return data"
The Problem
// ❌ ERROR: No code at all
// (Empty code field)
// ❌ ERROR: Code executes but doesn't return anything
const items = $input.all();
// Process items
for (const item of items) {
console.log(item.json.name);
}
// Forgot to return!
// ❌ ERROR: Early return path exists, but not all paths return
const items = $input.all();
if (items.length === 0) {
return []; // ✅ This path returns
}
// Process items
const processed = items.map(item => ({json: item.json}));
// ❌ Forgot to return processed!
The Solution
// ✅ CORRECT: Always return data
const items = $input.all();
// Process items
const processed = items.map(item => ({
json: {
...item.json,
processed: true
}
}));
return processed; // ✅ Return statement present
// ✅ CORRECT: Return empty array if no items
const items = $input.all();
if (items.length === 0) {
return []; // Valid: empty array when no data
}
// Process and return
return items.map(item => ({json: item.json}));
// ✅ CORRECT: All code paths return
const items = $input.all();
if (items.length === 0) {
return [];
} else if (items.length === 1) {
return [{json: {single: true, data: items[0].json}}];
} else {
return items.map(item => ({json: item.json}));
}
// All paths covered
Checklist
- Code field is not empty
- Return statement exists
- ALL code paths return data (if/else branches)
- Return format is correct (
[{json: {...}}]) - Return happens even on errors (use try-catch)
Error #2: Expression Syntax Confusion
What Happens — there are two distinct cases, and only one is a syntax error:
{{ }}inside a string literal: valid JavaScript that runs fine, but you get the literal text{{ ... }}instead of a value — n8n does not evaluate expressions inside Code-node code. A logic bug, not a validation error. (n8n-mcp ≥ 2.63.0 no longer flags{{ }}inside string literals — prompt templates, payload placeholders, and.replace()tokens are legitimate.){{ }}written as bare code (e.g.return {{ $json.x }}): a genuine JavaScript syntax error. The validator reports "Expression syntax {{...}} is not valid in Code nodes" and n8n throws "Unexpected token".
The Problem
n8n has TWO distinct syntaxes:
- Expression syntax
{{ }}- Used in OTHER nodes (Set, IF, HTTP Request) - JavaScript - Used in CODE nodes
Many developers mistakenly reach for expression syntax inside a Code node when they want a value. Putting {{ }} in a string does not interpolate it:
// ❌ LOGIC BUG: n8n never evaluates {{ }} in Code-node strings
const userName = "{{ $json.name }}";
const userEmail = "{{ $json.body.email }}";
return [{
json: {
name: userName,
email: userEmail
}
}];
// Result: Literal string "{{ $json.name }}", NOT the value!
// (This runs — it just doesn't do what you meant. Use $json.name directly.)
// ❌ SYNTAX ERROR: {{ }} used as code, not inside a string
const value = {{ $now.toFormat('yyyy-MM-dd') }}; // "Unexpected token"
The Solution
// ✅ CORRECT: Use JavaScript directly (no {{ }})
const userName = $json.name;
const userEmail = $json.body.email;
return [{
json: {
name: userName,
email: userEmail
}
}];
// ✅ CORRECT: JavaScript template literals (use backticks)
const message = `Hello, ${$json.name}! Your email is ${$json.email}`;
return [{
json: {
greeting: message
}
}];
// ✅ CORRECT: Direct variable access
const item = $input.first().json;
return [{
json: {
name: item.name,
email: item.email,
timestamp: new Date().toISOString() // JavaScript Date, not {{ }}
}
}];
Comparison Table
| Context | Syntax | Example |
|---|---|---|
| Set node | {{ }} expressions |
{{ $json.name }} |
| IF node | {{ }} expressions |
{{ $json.age > 18 }} |
| HTTP Request URL | {{ }} expressions |
{{ $json.userId }} |
| Code node | JavaScript | $json.name |
| Code node strings | Template literals | `Hello ${$json.name}` |
Quick Fix Guide
// WRONG → RIGHT conversions
// ❌ "{{ $json.field }}"
// ✅ $json.field
// ❌ "{{ $now }}"
// ✅ new Date().toISOString()
// ❌ "{{ $node['HTTP Request'].json.data }}"
// ✅ $node["HTTP Request"].json.data
// ❌ `{{ $json.firstName }} {{ $json.lastName }}`
// ✅ `${$json.firstName} ${$json.lastName}`
Error #3: Return Shape
What actually happens — n8n is more forgiving than the old advice implied:
- In Run Once for All Items mode, n8n auto-normalizes a single bare object, or an array of bare objects, by wrapping each under a
jsonproperty. So these run. - What genuinely fails, with "Code doesn't return items properly", is returning a primitive (string/number/boolean) or
null/undefined— there is nothing to wrap.
(n8n-mcp ≥ 2.63.0 no longer errors "Return value must be an array of objects" on a bare-object return; the earlier claim contradicted n8n's auto-wrap behavior.)
Prefer the Canonical Form
The canonical [{json: {...}}] is unambiguous and behaves identically in both execution modes, so make it your default even though looser shapes are auto-wrapped:
// ⚠️ Auto-wrapped → [{json: {result: 'success'}}]. Runs, but prefer the array + json form.
return {
json: {
result: 'success'
}
};
// ⚠️ Auto-wrapped → each object gets a json wrapper. Runs, but be explicit.
return [
{id: 1, name: 'Alice'},
{id: 2, name: 'Bob'}
];
// ✅ Fine — input items already carry a json property; returning them unchanged is a valid passthrough
return $input.all();
// ❌ FAILS: primitive value — nothing to wrap into an item
return "processed";
// ❌ FAILS: null / undefined — no items to pass on
return null;
The Solution
// ✅ CORRECT: Single result
return [{
json: {
result: 'success',
timestamp: new Date().toISOString()
}
}];
// ✅ CORRECT: Multiple results
return [
{json: {id: 1, name: 'Alice'}},
{json: {id: 2, name: 'Bob'}},
{json: {id: 3, name: 'Carol'}}
];
// ✅ CORRECT: Transforming array
const items = $input.all();
return items.map(item => ({
json: {
id: item.json.id,
name: item.json.name,
processed: true
}
}));
// ✅ CORRECT: Empty result
return [];
// Valid when no data to return
// ✅ CORRECT: Conditional returns
if (shouldProcess) {
return [{json: {result: 'processed'}}];
} else {
return [];
}
Return Format Checklist
- Return value is an array
[...](canonical — preferred) - Each array element has a
jsonproperty - Structure is
[{json: {...}}]or[{json: {...}}, {json: {...}}] - Not a primitive (string/number/boolean) or
null/undefined— those are the shapes that actually fail
Common Scenarios
// Scenario 1: Single object from API
const response = $input.first().json;
// ✅ CANONICAL
return [{json: response}];
// ⚠️ Auto-wrapped to the same thing — runs, but prefer the array form
return {json: response};
// Scenario 2: Array of objects
const users = $input.all();
// ✅ CANONICAL
return users.map(user => ({json: user.json}));
// ✅ Also fine — a passthrough of items that already carry json
return users;
// Scenario 3: Computed result
const total = $input.all().reduce((sum, item) => sum + item.json.amount, 0);
// ✅ CANONICAL
return [{json: {total}}];
// ⚠️ Auto-wrapped → [{json: {total}}] in All Items mode — runs, but be explicit
return {total};
// Scenario 4: No results
// ✅ CORRECT
return [];
// ❌ FAILS — null has no items to pass on
return null;
Error #4: Broken Strings & Escaping (JavaScript syntax errors)
What Happens:
- The Code node throws a JavaScript syntax error at execution: "Unexpected token" or "Unexpected end of input"
- Cause is your own JS — unbalanced quotes or a raw newline inside a plain quoted string
This is a plain JavaScript concern, not a validator check. The validator (n8n-mcp ≥ 2.63.0) does not flag balanced apostrophes, { } in regex, or {{ }} sitting inside a string literal — those are all valid JavaScript. Only genuinely malformed JS throws, and it throws at runtime.
The Problem
This happens when:
- A quote inside a same-quoted string is left unescaped
- A plain (non-template) string spans multiple lines
- Backslashes in paths/regex are not escaped
// ✅ FINE: an apostrophe inside a double-quoted string is valid JavaScript
const message = "It's a nice day";
// ✅ FINE: braces in a regex literal are valid
const pattern = /\{(\w+)\}/;
// ❌ SYNTAX ERROR: raw newline + unescaped inner double-quotes in a plain string
const html = "
<div class="container">
<p>Hello</p>
</div>
";
The Solution
// ✅ Mix quote styles or escape, whichever reads cleaner
const message = "It's a nice day"; // double quotes around an apostrophe — fine
const other = 'She said "hello"'; // single quotes around double quotes — fine
// ✅ Regex literals need no extra escaping of their own braces
const pattern = /\{(\w+)\}/;
// ✅ CORRECT: Template literals for multi-line
const html = `
<div class="container">
<p>Hello</p>
</div>
`;
// Backticks handle multi-line and quotes
// ✅ CORRECT: Escape backslashes
const path = "C:\\\\Users\\\\Documents\\\\file.txt";
Escaping Guide
| Character | Escape As | Example |
|---|---|---|
| Single quote in single-quoted string | \\' |
'It\\'s working' |
| Double quote in double-quoted string | \\" |
"She said \\"hello\\"" |
| Backslash | \\\\ |
"C:\\\\path" |
| Newline | \\n |
"Line 1\\nLine 2" |
| Tab | \\t |
"Column1\\tColumn2" |
Best Practices
// ✅ BEST: Use template literals for complex strings
const message = `User ${name} said: "Hello!"`;
// ✅ BEST: Use template literals for HTML
const html = `
<div class="${className}">
<h1>${title}</h1>
<p>${content}</p>
</div>
`;
// ✅ BEST: Use template literals for JSON
const jsonString = `{
"name": "${name}",
"email": "${email}"
}`;
Error #5: Missing Null Checks / Undefined Access
Frequency: Very common runtime error
What Happens:
- Workflow execution stops
- Error: "Cannot read property 'X' of undefined"
- Error: "Cannot read property 'X' of null"
- Crashes on missing data
The Problem
// ❌ WRONG: No null check - crashes if user doesn't exist
const email = item.json.user.email;
// ❌ WRONG: Assumes array has items
const firstItem = $input.all()[0].json;
// ❌ WRONG: Assumes nested property exists
const city = $json.address.city;
// ❌ WRONG: No validation before array operations
const names = $json.users.map(user => user.name);
The Solution
// ✅ CORRECT: Optional chaining
const email = item.json?.user?.email || 'no-email@example.com';
// ✅ CORRECT: Check array length
const items = $input.all();
if (items.length === 0) {
return [];
}
const firstItem = items[0].json;
// ✅ CORRECT: Guard clauses
const data = $input.first().json;
if (!data.address) {
return [{json: {error: 'No address provided'}}];
}
const city = data.address.city;
// ✅ CORRECT: Default values
const users = $json.users || [];
const names = users.map(user => user.name || 'Unknown');
// ✅ CORRECT: Try-catch for risky operations
try {
const email = item.json.user.email.toLowerCase();
return [{json: {email}}];
} catch (error) {
return [{
json: {
error: 'Invalid user data',
details: error.message
}
}];
}
Safe Access Patterns
// Pattern 1: Optional chaining (modern, recommended)
const value = data?.nested?.property?.value;
// Pattern 2: Logical OR with default
const value = data.property || 'default';
// Pattern 3: Ternary check
const value = data.property ? data.property : 'default';
// Pattern 4: Guard clause
if (!data.property) {
return [];
}
const value = data.property;
// Pattern 5: Try-catch
try {
const value = data.nested.property.value;
} catch (error) {
const value = 'default';
}
Webhook Data Safety
// Webhook data requires extra safety
// ❌ RISKY: Assumes all fields exist
const name = $json.body.user.name;
const email = $json.body.user.email;
// ✅ SAFE: Check each level
const body = $json.body || {};
const user = body.user || {};
const name = user.name || 'Unknown';
const email = user.email || 'no-email';
// ✅ BETTER: Optional chaining
const name = $json.body?.user?.name || 'Unknown';
const email = $json.body?.user?.email || 'no-email';
Array Safety
// ❌ RISKY: No length check
const items = $input.all();
const firstId = items[0].json.id;
// ✅ SAFE: Check length
const items = $input.all();
if (items.length > 0) {
const firstId = items[0].json.id;
} else {
// Handle empty case
return [];
}
// ✅ BETTER: Use $input.first()
const firstItem = $input.first();
const firstId = firstItem.json.id; // Built-in safety
Object Property Safety
// ❌ RISKY: Direct access
const config = $json.settings.advanced.timeout;
// ✅ SAFE: Step by step with defaults
const settings = $json.settings || {};
const advanced = settings.advanced || {};
const timeout = advanced.timeout || 30000;
// ✅ BETTER: Optional chaining
const timeout = $json.settings?.advanced?.timeout ?? 30000;
// Note: ?? (nullish coalescing) vs || (logical OR)
Error #6: UnsupportedFunctionError (Auth Helpers Blocked)
Frequency: The most common "this worked yesterday in old n8n" error after upgrading to v2.0+
What Happens:
- Error:
UnsupportedFunctionError: The function "helpers.httpRequestWithAuthentication" is not supported in the Code Node - Same for
helpers.requestWithAuthenticationPaginated - Throws on execution, not on save
The Problem
Since n8n v2.0, Code nodes execute in the task runner sandbox which deliberately blocks the auth helpers. The legacy vm2 sandbox used to bind them, which is why old forum posts and tutorials show them working. n8n's source comment explains why: the Code node has no credential of its own, so the helper had nothing to authenticate against — it was always semantically broken, just not always loud about it.
// ❌ BLOCKED in task runner sandbox (default since v2.0)
const data = await this.helpers.httpRequestWithAuthentication.call(
this,
'baseLinkerApi',
{ url: '...', method: 'POST' }
);
The Solution
There is no env flag to re-enable these in the runner — the deny-list is compiled-in. Pick one of:
Option A — Replace the Code node with an HTTP Request node (best):
The HTTP Request node natively supports credential attachment with full expression support for URL/body/headers. Most "Code-node-makes-an-API-call" patterns are leftovers from before HTTP Request had pagination and expression support.
Option B — Sub-workflow with HTTP Request node (when you need code-level logic before/after):
// Parent Code node — prepare payloads, then delegate
return $input.all().map(i => ({ json: {
url: 'https://api.example.com/things',
method: 'POST',
body: { sku: i.json.sku }
}}));
Then wire to Execute Workflow → child workflow with Execute Workflow Trigger → HTTP Request node using ={{ $json.url }}, ={{ $json.body }}, with the credential attached natively.
Option C — Token as runtime data (only when the token genuinely flows through the workflow):
// ✅ Works — manual auth header, token came from upstream
const token = $('Get Token').first().json.access_token;
const data = await this.helpers.httpRequest({
url: 'https://api.example.com/data',
headers: { 'Authorization': `Bearer ${token}` }
});
Decision Guide
| Need | Use |
|---|---|
| Single authenticated API call | HTTP Request node directly |
| Many API calls + pre/post processing | Sub-workflow pattern (Option B) |
| Token already in the data flow | Manual this.helpers.httpRequest() with header |
httpRequestWithAuthentication |
Doesn't work — pick A, B, or C above |
Error #7: $env is not defined / Cannot access $env
Frequency: Common in hardened production instances
What Happens:
- Error:
$env is not definedorReferenceError: $env is not defined - Code looks correct, runs fine on dev instance, throws in production
The Problem
$env access is gated by the N8N_BLOCK_ENV_ACCESS_IN_NODE environment variable. When set to true (a common production hardening setting), $env is removed from the Code node sandbox entirely. This is increasingly the default in security-conscious deployments.
// ❌ Throws if N8N_BLOCK_ENV_ACCESS_IN_NODE=true
const apiKey = $env.API_KEY;
The Solution
Treat secrets as a credential concern, not a Code-node concern:
// ✅ Token arrives as data from an upstream node that used a credential
const apiKey = $('Set Secret').first().json.apiKey;
// Or: secret was attached server-side by an HTTP Request node with the credential
// — your Code node never sees the raw secret, which is the whole point
For values you genuinely need to inject from outside the workflow (config, not secrets), use:
- A Set node at the top of the workflow with hardcoded constants, or
- An n8n credential referenced by an HTTP Request node, or
- The External Secrets integration (
$secrets) if your edition supports it.
Why This Matters
Skills and tutorials written before 2024 routinely use $env.API_KEY because it was the path of least resistance. Modern n8n setups block it because letting Code nodes read arbitrary env vars is a privilege escalation surface — any user with workflow-edit access could exfiltrate DB_PASSWORD, N8N_ENCRYPTION_KEY, etc. Don't fight the restriction; route secrets through credentials.
Error Prevention Checklist
Use this checklist before deploying Code nodes:
Code Structure
- Code field is not empty
- Return statement exists
- All code paths return data
Return Format
- Returns items, not a primitive/
null - Canonical shape
[{json: {...}}](bare objects auto-wrap, but be explicit)
Syntax
- No
{{ }}written as code (it's for other nodes' fields; in a string it's just literal text) - Template literals use backticks:
`${variable}` - All quotes and brackets balanced
- Strings properly escaped
Data Safety
- Null checks for optional properties
- Array length checks before access
- Webhook data accessed via
.body - Try-catch for risky operations
- Default values for missing data
Testing
- Test with empty input
- Test with missing fields
- Test with unexpected data types
- Check browser console for errors
Quick Error Reference
| Error Message | Likely Cause | Fix |
|---|---|---|
| "Code cannot be empty" | Empty code field | Add meaningful code |
| "Code must return data" | Missing return statement | Add return [...] |
| "Code doesn't return items properly" | Returned a primitive (string/number) or null |
Return [{json:{...}}] (objects/arrays auto-wrap; primitives don't) |
| "Not all items have a json key" | Mixed return — some items wrapped, some bare | Wrap every item: {json: {...}} |
| "Expression syntax {{...}} is not valid in Code nodes" / "Unexpected token" | {{ }} written as code (not inside a string) |
Use JavaScript: $json.x or `${$json.x}` |
| "Cannot read property X of undefined" | Missing null check | Use optional chaining ?. |
| "Cannot read property X of null" | Null value access | Add guard clause or default |
| "Unexpected end of input" | Unbalanced quotes/brackets in your JS | Escape strings or use backtick template literals |
| "UnsupportedFunctionError ... httpRequestWithAuthentication" | Auth helper blocked in task runner | Use HTTP Request node + credential, or sub-workflow pattern (Error #6) |
| "$env is not defined" | N8N_BLOCK_ENV_ACCESS_IN_NODE=true |
Route secrets through credentials, not $env (Error #7) |
| "Cannot find module 'crypto'" | require() allowlist not set |
Move logic out of Code node, or set N8N_RUNNERS_ALLOWED_BUILT_IN_MODULES |
Debugging Tips
1. Use console.log()
const items = $input.all();
console.log('Items count:', items.length);
console.log('First item:', items[0]);
// Check browser console (F12) for output
2. Return Intermediate Results
// Debug by returning current state
const items = $input.all();
const processed = items.map(item => ({json: item.json}));
// Return to see what you have
return processed;
3. Try-Catch for Troubleshooting
try {
// Your code here
const result = riskyOperation();
return [{json: {result}}];
} catch (error) {
// See what failed
return [{
json: {
error: error.message,
stack: error.stack
}
}];
}
4. Validate Input Structure
const items = $input.all();
// Check what you received
console.log('Input structure:', JSON.stringify(items[0], null, 2));
// Then process
Summary
Top 7 Errors to Avoid:
- Empty code / missing return - Always return data
- Expression syntax as code - Use JavaScript, not
{{ }}(in-string{{ }}is just literal text) - Return shape - prefer
[{json: {...}}]; primitives/nullfail (bare objects auto-wrap) - Broken strings - unbalanced quotes/brackets throw a JS syntax error; escape or use template literals
- Missing null checks - Use optional chaining
?. httpRequestWithAuthenticationblocked - Use HTTP Request node + credential$envblocked - Route secrets through credentials, not env access
Quick Prevention:
- Prefer the canonical
[{json: {...}}]return; never return a primitive ornull - Write JavaScript — don't put
{{ }}where code belongs - Check for null/undefined before accessing
- Test with empty and invalid data
- Use browser console for debugging
See Also:
- SKILL.md - Overview and best practices
- DATA_ACCESS.md - Safe data access patterns
- COMMON_PATTERNS.md - Working examples