JavaScript — Engineering
Notes for Working Code
A working reference written from the angle of shipping production JavaScript — the language itself, then async, performance, modules and tooling. Compact on theory, dense on the patterns that survive contact with real systems.
JavaScript executes in two main hosts: a browser (with a window and a DOM) and a server runtime like Node.js. The console is universal across both.
// Browser or Node — both support console console.log("hello"); console.log("value=", { id: 1, ok: true }); // Browser-only — the DOM is a tree of nodes const el = document.getElementById("out"); el.textContent = "rendered from JS";
<script> tag at the end of <body>, mark it defer, or wrap DOM code in a DOMContentLoaded listener.<!doctype html>
<html>
<body>
<p id="out">loading…</p>
<script>
document.getElementById("out").textContent = "ready";
</script>
</body>
</html>const name = "omar"; // block-scoped, cannot rebind let count = 0; // block-scoped, reassignable var legacy = 42; // function-scoped, hoisted — avoid in new code count++; // name = "x"; // TypeError: Assignment to constant variable
const. Promote to let the moment you genuinely need to rebind. Treat var as a code-smell in modern code.let saferfunction demo() { if (true) { let inner = 1; var outer = 2; } // inner -> ReferenceError (block-scoped) // outer -> 2 (hoisted to function scope) }
const with objects: the binding is frozen, the value isn'tconst user = { name: "omar" }; user.name = "rupam"; // allowed — object is mutable // user = {}; // TypeError — re-binding blocked // To freeze contents too: const config = Object.freeze({ env: "prod" });
true, false // boolean literals null // intentional absence of any value undefined // declared but not yet assigned NaN // "not a number" — result of failed numeric ops Infinity, -Infinity Number.MAX_SAFE_INTEGER // 2^53 - 1 Number.EPSILON // smallest gap between two doubles Math.PI, Math.E
NaN is the only JavaScript value not equal to itself. Test with Number.isNaN(x), never x === NaN.// single line — for short context /* block comment — for paragraphs */ /** * Multiply two numbers. * @param {number} a * @param {number} b * @returns {number} */ function mul(a, b) { return a * b; }
console.log — the diagnostic surface most engineers underuse.console.log("info"); console.warn("yellow triangle"); console.error("red, with stack trace"); console.debug("verbose level"); // Tabular display — great for arrays of objects console.table([{ id: 1, name: "a" }, { id: 2, name: "b" }]); // Grouping console.group("request"); console.log("url", url); console.log("status", res.status); console.groupEnd(); // Timers console.time("db"); await db.query("…"); console.timeEnd("db");
typeof table that catches most beginners.| Type | Category | typeof | Example |
|---|---|---|---|
| string | primitive | "string" | "omar" |
| number | primitive | "number" | 42, 3.14, NaN |
| bigint | primitive | "bigint" | 9007199254740993n |
| boolean | primitive | "boolean" | true |
| undefined | primitive | "undefined" | undefined |
| null | primitive | "object" (legacy) | null |
| symbol | primitive | "symbol" | Symbol("id") |
| object | reference | "object" / "function" | {}, [], fn |
typeof null === "object" is a historical bug that is now part of the spec. To check explicitly: x === null.const a = { n: 1 }; const b = a; // b is a reference to the same object b.n = 9; console.log(a.n); // 9 — they share state
// Template literals — multi-line, interpolation const name = "omar"; const msg = `hello, ${name.toUpperCase()}`; // Searching "foobar".includes("oba"); // true "foobar".startsWith("foo"); // true "foobar".indexOf("bar"); // 3 // Transforming " trim me ".trim(); "a,b,c".split(","); // ["a","b","c"] ["a","b","c"].join("|"); // "a|b|c" // Padding / repeating "7".padStart(3, "0"); // "007" "ha".repeat(3); // "hahaha"
"𝟚".length === 2 — astral-plane characters count as two UTF-16 code units. For real character counts use [...str].length or Intl.Segmenter.Date object, ISO strings, and the case for Intl / Temporal.const now = new Date(); // current instant const t1 = Date.now(); // ms since epoch const iso = now.toISOString(); // "2026-05-14T12:00:00.000Z" // Parsing — accepts ISO 8601 reliably const d = new Date("2026-05-14T09:00:00Z"); // Locale-aware formatting new Intl.DateTimeFormat("en-GB", { dateStyle: "long", timeStyle: "short" }).format(now);
Date object is mutable and has surprising edge cases (months are 0-indexed, parsing of non-ISO strings is implementation-defined). For non-trivial work prefer Temporal (now in stage 3) or a vetted library.const xs = [1, 2, 3]; xs[0]; // 1 xs.length; // 3 xs.push(4); // append xs.unshift(0); // prepend xs.pop(); // remove + return last xs.shift(); // remove + return first
const nums = [1, 2, 3, 4]; nums.map(n => n * 2); // [2,4,6,8] nums.filter(n => n % 2); // [1,3] nums.reduce((a, n) => a + n, 0); // 10 nums.find(n => n > 2); // 3 // Non-mutating sort/reverse (ES2023) nums.toSorted((a, b) => b - a); // new array, original untouched
const a = [1, 2, 3]; const b = [...a]; // shallow copy const c = a.slice(); // same thing, older syntax const d = [...a, 4, 5]; // concat // Destructuring const [first, ...rest] = a;
const user = { id: 1, name: "omar", greet() { return `hi ${this.name}`; }, }; user.name; // dot access user["name"]; // bracket access (dynamic keys) user?.profile?.bio; // optional chaining user.role ?? "guest"; // nullish coalescing // Spread + override const patched = { ...user, name: "rupam" }; // Destructuring with rename + default const { name: who, role = "guest" } = user;
{...obj}) is shallow — nested objects are still shared. For deep copies use structuredClone(obj).// Map — keyed by anything, preserves insertion order const m = new Map(); m.set("x", 1); m.set({ id: 7 }, "object as key"); m.get("x"); m.has("x"); m.size; // Set — unique values, fast membership check const s = new Set([1, 1, 2, 3]); s.size; // 3 s.has(2); [...s]; // back to array
Map when keys are dynamic, non-string, or you need size. Use plain objects for fixed, known-at-write-time keys and JSON interop.// Declaration — hoisted function add(a, b) { return a + b; } // Expression — not hoisted, can be anonymous const sub = function (a, b) { return a - b; }; // Arrow — concise, no own this/arguments const mul = (a, b) => a * b;
function greet(name = "friend", ...titles) { return `hello ${titles.join(" ")} ${name}`; } greet(); // "hello friend" greet("omar", "Mr."); // "hello Mr. omar"
function counter() { let n = 0; return () => ++n; } const next = counter(); next(); next(); next(); // 3
class Account { #balance = 0; // private field constructor(owner) { this.owner = owner; } deposit(amount) { if (amount <= 0) throw new Error("non-positive"); this.#balance += amount; } get balance() { return this.#balance; } static from(owner, opening) { const a = new Account(owner); a.deposit(opening); return a; } } const a = Account.from("omar", 100); a.balance; // 100
Every JavaScript object has an internal link to another object — its prototype. Property lookups walk that chain until they find a match or hit null.
const animal = { eats() { return "chewing"; } }; const dog = Object.create(animal); dog.eats(); // "chewing" — looked up via prototype Object.getPrototypeOf(dog) === animal; // true // class X extends Y { ... } is the same machinery // with nicer syntax.
this actually is- Method call —
obj.fn()→this === obj - Plain call —
fn()→undefinedin strict mode,globalThisotherwise - Constructor call —
new Fn()→ fresh instance - Explicit bind —
fn.call(x)/.apply(x)/.bind(x)→ whatever you pass
const user = { name: "omar", greet() { return this.name; }, greetArrow: () => this?.name, }; user.greet(); // "omar" — method call const f = user.greet; f(); // undefined — detached, strict-mode plain call user.greetArrow(); // undefined — arrow captures enclosing this
this. They capture it from where they're defined, not where they're called. Don't use arrows for object methods that need this.const p = new Promise((resolve, reject) => { setTimeout(() => resolve("done"), 100); }); p.then(value => console.log(value)) // "done" .catch(err => console.error(err)) .finally(() => console.log("settled"));
| Method | Resolves when | Rejects when |
|---|---|---|
| Promise.all | all fulfil | any rejects (fail-fast) |
| Promise.allSettled | all settle | never — always resolves |
| Promise.race | first settles | first settles, if rejection |
| Promise.any | first fulfils | all reject (AggregateError) |
async function loadUser(id) { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } // Parallel — start both before awaiting either const [a, b] = await Promise.all([ loadUser(1), loadUser(2), ]);
awaits inside a loop are the most common performance trap in async JavaScript. If iterations are independent, build the array of promises and await Promise.all outside the loop.try { const data = JSON.parse(input); use(data); } catch (err) { if (err instanceof SyntaxError) throw new Error("bad json", { cause: err }); throw err; // re-raise anything you can't handle } finally { cleanup(); } // Custom error type — preferred over passing strings around class NotFoundError extends Error { constructor(resource) { super(`not found: ${resource}`); this.name = "NotFoundError"; } }
// Literal vs constructor const rx1 = /^foo\d+$/; const rx2 = new RegExp("^foo\\d+$"); // Common ops "hello world".match(/o/g); // ["o","o"] "hello world".replace(/o/g, "0"); // "hell0 w0rld" /\d+/.test("abc123"); // true // Named capture groups const m = "2026-05-14".match(/^(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})$/); m.groups.y; // "2026"
x flag (where supported) or the verbose-regex trick (split into a string built from comment-decorated parts) for any expression that doesn't fit on one line. Future-you will thank present-you.Measure first
Profile in the actual host before optimising. performance.now() and the browser profiler beat intuition.
Avoid layout thrash
Read all DOM measurements, then write. Interleaving forces synchronous reflow on every cycle.
Batch work
Group async writes with requestAnimationFrame or microtasks. Coalesce events with debounce / throttle.
Ship less
Code-split, lazy-load, tree-shake. The fastest code is the code that never reaches the browser.
// Microbenchmark template const t0 = performance.now(); for (let i = 0; i < 1e6; i++) work(i); const t1 = performance.now(); console.log(`took ${(t1 - t0).toFixed(2)} ms`);
// Hard breakpoint — pauses if devtools are open debugger; // Watch a value tree without spreading console.dir(node); // inspectable object view // Trace where a call came from console.trace("called from"); // Conditional logs without rebuilding // (Right-click line gutter in devtools → "Add logpoint")
// math.js export function add(a, b) { return a + b; } export const PI = 3.14159; export default function main() { /* ... */ } // caller.js import main, { add, PI } from "./math.js"; import * as math from "./math.js";
async function onClick() { const { renderChart } = await import("./chart.js"); renderChart(); }
import() when payload size matters or the dependency is conditional (heavy editor, charting, route-specific bundle).// still common in Node packages const { add } = require("./math"); module.exports = { add };
Package manager
npm, pnpm, or bun. Lockfile committed. Workspaces for monorepos.
Bundler
esbuild / vite / rspack. Fast dev, tree-shake, code-split, source maps in prod.
Linter + formatter
eslint for correctness, prettier (or biome) for layout. Run in CI as gates, not opinions.
Tests
vitest / jest for unit, playwright for end-to-end. Fast feedback or no feedback.
TypeScript
Even in a JS codebase, run TS in checkJs mode via JSDoc — catches a surprising share of bugs before tests do.
CI gates
Type-check, lint, test, build — all four green before merge. Never skip a gate to ship.
{
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest run",
"lint": "eslint . --max-warnings=0",
"typecheck":"tsc --noEmit",
"check": "npm run lint && npm run typecheck && npm run test"
}
}