Best Practices

This page suggests additional best practices for working with JavaScript in Make.com workflows.

Code organization

Use descriptive variable names

Clear variable names are a first step to making the code self-documenting. This helps others (and your future self) understand the logic quickly without guessing.

AvoidUse Descriptive Names
Code
// Hard to understand
const e = input.email;
const t = input.items.reduce((s, i) => s + i.price, 0);
// Clear and descriptive
const customerEmail = input.email;
const orderTotal = input.items.reduce((sum, item) => sum + item.price, 0);
ResultShort variable names make code cryptic. Other developers (or your future self) will struggle to understand the logic.Descriptive names make the purpose of each variable obvious. Code is easier to read, maintain, and debug.

Add code comments for complex logic

Comments explain why the code exists, not just what it does.

Without CommentsWith Comments
Code
// No explanation of logic
const taxRate = input.country === 'US' ? 0.08 : 0.20;
const taxAmount = orderTotal * taxRate;

return { subtotal: orderTotal, tax: taxAmount, total: orderTotal + taxAmount };

// Calculate tax based on customer location
const taxRate = input.country === 'US' ? 0.08 : 0.20;
const taxAmount = orderTotal * taxRate;

return { subtotal: orderTotal, tax: taxAmount, total: orderTotal + taxAmount };

ResultCode works, but the reasoning behind it is unclear. Anyone reading it must figure out the business logic from scratch.Comments clarify *why* the logic exists. Makes onboarding, debugging, and collaboration much faster.

Validate input data

Validating inputs prevents runtime errors and unexpected crashes.

Without ValidationWith Validation
Code
// Assumes all inputs are correct
const totalItems = input.items.length;
const user = input.userId;
// Always validate critical inputs
if (!input.userId || typeof input.userId !== 'string') {
  return { error: "Valid userId is required" };
}

if (!Array.isArray(input.items) || input.items.length === 0) { return { error: "Items array cannot be empty" }; }

// Continue with processing...

ResultRisk of runtime errors (e.g., calling length on undefined). Can break the entire workflow if inputs are invalid.Invalid inputs are caught early. Workflow fails gracefully with clear error messages instead of crashing.

Error handling

Use try-catch for external API calls

External APIs can fail for reasons outside your control (timeouts, invalid payloads, network errors). Wrapping calls in try…catch ensures failures don’t crash the workflow but return structured errors you can handle.

Without Try-CatchWith Try-Catch
Code
// No error handling
const response = await fetch(input.apiUrl, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(input.data)
});
const result = await response.json();
return { success: true, data: result };
// With error handling
try {
  const response = await fetch(input.apiUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(input.data)
  });

const result = await response.json(); return { success: true, data: result };

} catch (error) { return { success: false, error: error.message, timestamp: new Date().toISOString() }; }

ResultAny API failure throws an exception. The workflow breaks with no useful output.Failures are caught and returned as structured error objects. The workflow continues gracefully with clear diagnostics.

Provide meaningful error messages

Specific error messages guide the user toward fixing the problem. Generic ones leave them guessing and slow down troubleshooting.

Generic Error MessageMeaningful Error Message
Code
// Generic: unclear message
if (!input.email.includes('@')) {
  return { error: "Invalid input" };
}
// Specific: tells user what’s wrong
if (!input.email.includes('@')) {
  return { error: "Email must contain @ symbol" };
}
ResultUser only knows something failed, but not why. Debugging requires trial and error.User gets actionable feedback (missing “@”). Fixing the error is quick and straightforward.

Performance tips

Avoid unnecessary loops

Array methods like .filter() and .map() are faster to write, easier to read, and less prone to errors than manual loops.

Manual LoopBuilt-in Array Methods
Code
// Verbose: manual loop
const activeUsers = [];
for (let i = 0; i < input.users.length; i++) {
  if (input.users[i].status === 'active') {
    activeUsers.push(input.users[i]);
  }
}
// Cleaner: use array methods
const activeUsers = input.users.filter(user => user.status === 'active');
const userEmails = activeUsers.map(user => user.email);
ResultCode is longer and harder to maintain. Easy to introduce off-by-one or push errors.Code is concise and expressive. Improves readability and reduces chance of mistakes.

Limit data processing

Processing only the fields you actually need will reduce payload size and makes outputs easier to handle in downstream modules.

Returning All DataReturning Only Essential Data
Code
// Inefficient: includes unnecessary fields
return { user: input.user };
// Efficient: only keep essential fields
const essentialData = {
  id: input.user.id,
  email: input.user.email,
  name: input.user.name
};

return { user: essentialData };

ResultPayload may contain large or irrelevant data. Downstream modules do extra work filtering it out.Output is lean and focused. Downstream modules process only what matters.

Handle edge cases

Handling missing or empty values prevents runtime errors and ensures workflows return reliable results.

Without Edge Case HandlingWith Edge Case Handling
Code
// Risky: assumes input.items always exists
const total = input.items.reduce((sum, item) => sum + item.price, 0);
return { total };
// Safe: handles missing/empty cases
const items = input.items || [];
const total = items.length > 0 
  ? items.reduce((sum, item) => sum + (item.price || 0), 0)
  : 0;

return { total, itemCount: items.length };

ResultCode throws an error if items is missing or empty. Workflow fails unexpectedly.Code works even if items is undefined or empty. Workflow always produces a safe result.

Testing

Before deploying your JavaScript in Make:

1

Test with sample data — Use the CustomJS platform to test your functions.

2

Verify return values — Ensure your return statement produces the expected output.

3

Check error scenarios — Test what happens with invalid or missing input.

4

Validate data types — Confirm your output matches expected data types.

Pitfalls to avoid

Forgetting async/await

Without await, you return a Promise instead of usable data. Make cannot work with unresolved Promises.

Without AwaitWith Await
Code
// Wrong: returns a Promise, not data
const data = fetch(url).then(r => r.json());
return { data };
// Correct: wait for the response
const response = await fetch(url);
const data = await response.json();
return { data };
ResultMake receives an unresolved Promise. The next module cannot use it.Make receives the actual JSON data. The workflow continues smoothly.

Modifying input directly

Inputs should be treated as immutable. Directly changing them can cause side effects and unexpected results. Creating a new object keeps data flow predictable.

Modifying InputCreating New Object
Code
// Avoid: modifies the input object
input.processed = true;
return input;
// Better: return a new object
return {
  ...input,
  processed: true,
  processedAt: new Date().toISOString()
};
ResultOriginal input is mutated. Other modules may behave unexpectedly.Original input stays intact. New object makes changes explicit and safe.