Build reliable frontend logic with values, scope, functions, collections, objects, modules, errors, and asynchronous control flow.
Understand JavaScript values, coercion, identity, and equality
Use lexical scope and closures intentionally
Transform arrays and objects without hidden mutation
Work with classes, prototypes, modules, and errors
Write predictable asynchronous code with promises and async/await
JavaScript has primitive values such as strings, numbers, booleans, bigint, symbol, undefined, and null, plus objects that hold collections of properties.
Dynamic typing means variables can reference values of different types over time, so clear naming and defensive checks matter at API boundaries.
Use typeof for basic runtime inspection
Remember that typeof null is historically 'object'
Prefer explicit conversions when coercion would be surprising
const rawAge = "26";
const age = Number(rawAge);
console.log(typeof rawAge); // "string"
console.log(typeof age); // "number"
const profile = {
name: "Ada",
active: true,
score: null,
};
const safeScore =
typeof profile.score === "number"
? profile.score
: 0;Primitive values are compared by value, while objects and arrays are compared by reference. Two objects with identical fields are still different references.
Frontend state tools often rely on reference changes to detect updates, so creating new arrays and objects is safer than mutating existing state-shaped data.
Use strict equality for predictable comparisons
Understand reference identity for arrays, objects, and functions
Create new values when updating immutable data
const firstName = "Ada";
const anotherName = "Ada";
console.log(firstName === anotherName); // true
const userA = { id: 1, name: "Ada" };
const userB = { id: 1, name: "Ada" };
const userC = userA;
console.log(userA === userB); // false
console.log(userA === userC); // true
const updatedUser = {
...userA,
name: "Grace",
};let and const are block-scoped, while var is function-scoped and follows older hoisting rules that can create confusing behavior.
Bindings declared with const cannot be reassigned, but objects referenced by const can still be mutated unless your code treats them immutably.
Use const by default
Use let only when reassignment is required
Avoid var in modern application code
const appName = "Learning Lab";
function createLabel(score) {
let status = "starting";
if (score >= 80) {
const message = "Great work!";
status = "passing";
console.log(message);
}
return `${appName}: ${status}`;
}
console.log(createLabel(92));A function retains access to variables from the lexical scope where it was created. This closure behavior powers event handlers, factories, memoization, and module privacy.
Closures capture bindings rather than frozen copies, so delayed callbacks can observe updated values.
Keep function inputs and outputs explicit
Use closures for narrow, intentional state
Avoid hidden mutation in reusable helpers
function createCounter(start = 0) {
let count = start;
return {
increment() {
count += 1;
return count;
},
decrement() {
count -= 1;
return count;
},
get value() {
return count;
},
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.value); // 11Arrow functions do not create their own this binding. They inherit this from the surrounding lexical scope.
Regular functions can receive this from how they are called, which matters for object methods, constructors, and event APIs.
Use arrow functions for callbacks and lexical this
Use method syntax when an object method needs dynamic this
Do not use arrow functions as constructors
const player = {
name: "Mina",
score: 5,
addPoint() {
this.score += 1;
const announce = () => {
console.log(`${this.name}: ${this.score}`);
};
announce();
},
};
player.addPoint(); // Mina: 6Array methods such as map, filter, find, some, every, and reduce express common data transformations without manually managing indexes.
Choose the method that matches the intent. This makes data pipelines easier to understand and reduces mutation.
Use map to transform items
Use filter to keep matching items
Use find when only one matching item is needed
const courses = [
{ id: 1, title: "React", completed: true },
{ id: 2, title: "JavaScript", completed: false },
{ id: 3, title: "CSS", completed: true },
];
const completedTitles = courses
.filter((course) => course.completed)
.map((course) => course.title);
console.log(completedTitles);
// ["React", "CSS"]Objects group named properties. Destructuring extracts selected values, while spread copies enumerable properties into a new object.
Spread is shallow, so nested objects still share references unless they are copied explicitly.
Use destructuring to make dependencies visible
Use spread for shallow immutable updates
Copy each nested level that changes
const user = {
id: 7,
name: "Ada",
preferences: {
theme: "dark",
compactMode: false,
},
};
const updatedUser = {
...user,
preferences: {
...user.preferences,
compactMode: true,
},
};
console.log(user.preferences.compactMode); // false
console.log(updatedUser.preferences.compactMode); // trueOptional chaining safely stops property access when a value is null or undefined. Nullish coalescing provides a fallback only for null or undefined.
This differs from ||, which also treats values such as 0, false, and an empty string as falsey.
Use ?. when part of an object path may be missing
Use ?? when zero, false, or empty strings are valid values
Avoid long optional chains that hide required data problems
const settings = {
retries: 0,
profile: {
displayName: "",
},
};
const retries = settings.retries ?? 3;
const displayName =
settings.profile?.displayName ?? "Anonymous";
console.log(retries); // 0
console.log(displayName); // ""JavaScript classes provide syntax over the language's prototype-based object model. Methods defined in a class are shared through the prototype.
Use classes when identity and behavior naturally belong together. For simple data transformation, plain functions and objects are often easier to compose.
Understand that class methods live on the prototype
Use private fields for truly private instance state
Prefer composition when inheritance adds unnecessary coupling
class BankAccount {
#balance = 0;
constructor(owner) {
this.owner = owner;
}
deposit(amount) {
if (amount <= 0) {
throw new Error("Deposit must be positive.");
}
this.#balance += amount;
}
get balance() {
return this.#balance;
}
}
const account = new BankAccount("Ada");
account.deposit(250);
console.log(account.balance); // 250ES modules let files explicitly export and import values. This makes dependencies visible and allows tooling to analyze module boundaries.
Prefer named exports for libraries with several related values and default exports only when a module has one obvious primary value.
Use export and import instead of global variables
Keep modules focused around a coherent responsibility
Avoid circular dependencies between modules
// math.js
export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
export const TAX_RATE = 0.07;
// checkout.js
import { clamp, TAX_RATE } from "./math.js";
const discount = clamp(0.15, 0, 0.5);
const total = 100 * (1 - discount) * (1 + TAX_RATE);
console.log(total);Throw errors when a function cannot fulfill its contract. Catch errors where your program has enough context to recover, report, retry, or translate them.
Avoid swallowing exceptions silently because doing so hides failures and complicates debugging.
Throw Error objects with useful messages
Catch at meaningful recovery boundaries
Use finally for cleanup that must always run
function parseSettings(json) {
try {
const settings = JSON.parse(json);
if (typeof settings !== "object" || settings === null) {
throw new Error("Settings must be an object.");
}
return { ok: true, value: settings };
} catch (error) {
return {
ok: false,
error:
error instanceof Error
? error.message
: "Unknown parsing error",
};
}
}A promise represents the eventual completion or failure of asynchronous work. Promise chains return new promises, allowing results and errors to flow through a sequence.
Always return promise-producing work from callbacks so callers can observe completion.
Use then for transformations and catch for failures
Return nested asynchronous work instead of creating detached promises
Use Promise.all for independent work
async function loadDashboard() {
const [profileResponse, statsResponse] = await Promise.all([
fetch("/api/profile"),
fetch("/api/stats"),
]);
if (!profileResponse.ok || !statsResponse.ok) {
throw new Error("Dashboard request failed.");
}
const [profile, stats] = await Promise.all([
profileResponse.json(),
statsResponse.json(),
]);
return { profile, stats };
}async functions always return promises. await pauses only the current async function until the awaited promise settles, making dependent asynchronous steps read more like synchronous code.
Use try/catch when the current layer can meaningfully recover or translate an asynchronous failure.
Return or await promises so callers observe completion
Keep sequential awaits for truly dependent operations
Abort stale requests when their result is no longer useful
async function fetchUser(userId, signal) {
const response = await fetch(`/api/users/${userId}`, {
signal,
headers: {
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(
`Failed to load user: ${response.status}`,
);
}
return response.json();
}
const controller = new AbortController();
fetchUser("42", controller.signal)
.then((user) => console.log(user))
.catch((error) => {
if (error.name !== "AbortError") {
console.error(error);
}
});JavaScript runs ordinary code on a call stack while the host environment schedules timers, network callbacks, and other asynchronous work.
Promise callbacks run as microtasks, which are processed before the next timer task. Understanding this order helps explain surprising console output.
Synchronous code runs before queued callbacks
Promise reactions use the microtask queue
Timers do not guarantee exact execution time
console.log("A");
setTimeout(() => {
console.log("D");
}, 0);
Promise.resolve().then(() => {
console.log("C");
});
console.log("B");
// Output:
// A
// B
// C
// D