You already know how to program. The danger is assuming JavaScript is Python with braces. It is not: missing values multiply, arrays are references, promises are ordinary values, and the runtime matters as much as the syntax. This is the migration briefing I would give a Python teammate before their first JavaScript pull request.
🎙️ Published & recorded:
The fastest way to write bad JavaScript is to transliterate a Python file. Translate the intent, not the surface. A Python dictionary often becomes an object, but a dictionary with arbitrary keys may need a Map. A list comprehension often becomes map plus filter, but a plain loop is frequently clearer. JavaScript also gives you two absences, null and undefined. Treat undefined as “not supplied or not found” and reserve null for deliberate emptiness.
# Python
names = [u["name"] for u in users if u.get("active")]
first = next((u for u in users if u["id"] == wanted_id), None)
// JavaScript: the question mark is optional chaining
const names = users.filter(u => u.active).map(u => u.name);
const first = users.find(u => u.id === wantedId); // undefined if absent
const city = first?.address?.city ?? "unknown";
===, not ==. The loose operator converts types before comparing, so 0 == false is true. Python trained you to expect comparison to be boring. Keep it boring with strict equality.const first; avoid varMy rule is blunt: declare with const first. Change it to let only when the name itself must be reassigned. Avoid var; its function scope and hoisting preserve historical mistakes you do not need to inherit. Do not confuse a constant binding with an immutable value. A const array can still be pushed into, just as a Python name can keep pointing at a mutated list.
const user = { name: "Ada" };
user.name = "Grace"; // valid: object mutated
user = { name: "Lin" }; // TypeError: Assignment to constant variable.
let retries = 3;
retries -= 1; // reassignment, so let is honest
// Do not write new code with var.
ReferenceError: Cannot access 'total' before initializationlet or const name above its declaration. Move the declaration before the first read. Do not “repair” this by changing it to var; that merely turns a loud bug into an undefined bug.Python developers get caught by exactly two values: empty arrays and empty objects are truthy in JavaScript. So if (items) answers “does this variable hold an array?” rather than “does it contain anything?” Strings still behave as you expect. Zero, the empty string, null, undefined, NaN, and false are falsy. Everything else is truthy.
# Python
if not items:
print("empty")
// JavaScript
if (items.length === 0) console.log("empty");
if (Object.keys(options).length === 0) console.log("no options");
const shown = count ?? 10; // preserves a legitimate 0
const wrong = count || 10; // replaces 0 with 10
value or default to value || default loses valid zeroes and empty strings. Use ?? when only null or undefined means missing.JavaScript arrays are mutable references, like Python lists, but their methods split into two camps. map, filter, slice, and toSorted return new arrays. push, pop, splice, and sort mutate the original. That last one causes excellent bugs because default sorting is textual: ten comes before two.
# Python
prices = [10, 2, 30]
ordered = sorted(prices) # [2, 10, 30]
last = prices[-1]
// JavaScript
const prices = [10, 2, 30];
const ordered = prices.toSorted((a, b) => a - b);
const last = prices.at(-1);
const doubled = prices.map(n => n * 2);
TypeError: Cannot read properties of undefined (reading 'name')find() missed, then the next line dereferenced the result. First log or inspect the value immediately before the failing dot. Then choose deliberately: guard with if (!user), return early, use user?.name for genuinely optional data, or throw a clearer error. Do not scatter optional chaining over data that is required.An object is ideal for a record with known string keys: a user, configuration, or API payload. For arbitrary keys, especially object keys, use Map. Property access can use dots or brackets. Brackets are required when the key lives in a variable. Destructuring replaces much of the repetitive dictionary unpacking you wrote in Python, and spread makes shallow copies. Emphasis on shallow: nested objects are still shared.
# Python
name = user["name"]
role = user.get("role", "reader")
updated = {**user, "active": True}
// JavaScript
const { name, role = "reader" } = user;
const updated = { ...user, active: true };
const field = "name";
console.log(user[field]); // variable key
const byElement = new Map(); // arbitrary keys
byElement.set(button, { clicks: 1 });
const copy = {...original}, changing copy.address.city also changes original.address.city. For JSON-like data that truly needs a deep copy, use structuredClone(original). Better yet, copy only the nested branch you intend to update.Functions are values in both languages, but JavaScript APIs lean hard on callbacks. Use declarations for substantial named behavior and arrows for short callbacks. JavaScript has no Python-style keyword arguments, so use an options object when positional arguments become cryptic. Destructure it in the parameter list and provide defaults there.
# Python
def fetch_users(active=True, limit=20):
...
fetch_users(limit=5)
// JavaScript: options object substitutes for keyword arguments
function fetchUsers({ active = true, limit = 20 } = {}) {
return client.get("/users", { active, limit });
}
fetchUsers({ limit: 5 });
const labels = users.map(user => user.name.toUpperCase());
TypeError: formatUser is not a functionformatUser with console.log(typeof formatUser, formatUser). If it is undefined, check the export/import names. If it is an object, you probably imported a module namespace or default export incorrectly. If you accidentally wrote onClick={formatUser()}, pass the function instead: onClick={formatUser}.thisPython’s self is explicit and stable. JavaScript’s this depends on how a function is called, which is a terrible fact to carry around while migrating. My recommendation: largely avoid this. Prefer plain functions, closures, and data passed as arguments. You still need to read this in classes and older libraries, but you do not need to design new application code around it.
// Fragile when passed as a callback: the receiver is lost.
const counter = {
value: 0,
increment() { this.value += 1; }
};
setTimeout(counter.increment, 100);
// Boring and portable: state is explicit.
function increment(state) {
return { ...state, value: state.value + 1 };
}
let state = { value: 0 };
state = increment(state);
counter.increment.bind(counter), or wrap the call: () => counter.increment(). Do not randomly convert every function to an arrow; understand which object should own the call.async/await: familiar syntax, different runtimeJavaScript’s event loop means an asynchronous function returns a Promise immediately. await pauses only the current async function, not the process. Fetch has a second trap: HTTP errors such as 404 do not reject the promise. You must check response.ok. Use Promise.all for independent work; sequential awaits in a loop are correct but needlessly slow when tasks do not depend on one another.
async function fetchJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json();
}
const [user, teams] = await Promise.all([
fetchJson("/api/users/42"),
fetchJson("/api/teams")
]);
Promise { <pending> }async function and write const user = await loadUser(42). At a module’s top level, modern ES modules allow top-level await. In older CommonJS code, use an async entry function and call it with a final .catch(console.error).await items.forEach(async item => save(item)) does not wait for the callbacks. Use await Promise.all(items.map(save)) for parallel work, or a for...of loop with await when order matters.Python has one mainstream import model. JavaScript has the modern ES module system and the older Node.js CommonJS system. New code should use ES modules: export and import. In Node, declare that choice with "type": "module" in package.json, or use the .mjs extension. In a browser, load the entry script with type="module". Mixing systems creates errors that look like syntax problems but are configuration problems.
// math.js
export function mean(values) {
return values.reduce((sum, n) => sum + n, 0) / values.length;
}
export const version = "1.0";
// report.js — relative imports need the extension in browsers
import { mean, version } from "./math.js";
console.log(mean([2, 4, 9]), version);
SyntaxError: Unexpected token 'export'"type": "module" to the nearest package.json or rename the file to .mjs. In HTML, use <script type="module" src="app.js"></script>. Then keep imports and exports consistently ESM; do not paper over it by swapping random lines to require.JavaScript stack traces are noisy, but the useful routine is the same as Python: read the first relevant frame in your code, inspect the value at that boundary, and reproduce with the smallest input. The browser console is not a substitute for understanding types. Ask typeof, Array.isArray, and whether a promise was awaited. Then fix the producer of the bad value, not every consumer downstream.
// Keep this five-question checklist beside the debugger.
value === undefined // missing property, index, return?
value === null // deliberate empty value?
typeof value // "function", "object", "string"...
Array.isArray(value) // typeof [] is misleadingly "object"
value instanceof Promise // forgot await?
// JSON is stricter than a JS object literal:
JSON.parse("{'name': 'Ada'}");
SyntaxError: Unexpected token ' in JSON at position 1{"name":"Ada"}. Better: do not construct JSON by hand. Build a JavaScript object and call JSON.stringify(data); when reading a response, verify the content type before calling response.json().The practical migration stance: use const, strict equality, explicit empty checks, plain functions, ES modules, and await every promise whose value you need. Avoid var, largely avoid this, and stop trying to make JavaScript impersonate Python. Once you accept its runtime model, the language becomes much less surprising.