Language Refresher

Variables & Types

๐Ÿ“ฆ Variable Declarations

โœ…
const
Block-scoped
Cannot reassign
RECOMMENDED
๐Ÿ‘
let
Block-scoped
Can reassign
USE WHEN NEEDED
โš ๏ธ
var
Function-scoped
Hoisted
AVOID

๐Ÿท๏ธ JavaScript Types (typeof results)

number string boolean undefined object function bigint symbol
โš ๏ธ Quirk: typeof null === "object" (historical bug)
  • Declarations: let, const (recommended), var (avoid).
  • Scope: let/const are block-scoped; var is function-scoped (or global).
  • Dynamic typing: the same variable can hold values of different types across time.
let x = 42;
x = "hello";
const PI = 3.14159;
var legacy = true; // avoid
function hello() {}
console.log(typeof x);       // "string"
console.log(typeof hello);   // "function"

Conditionals

const age = 20;
const hasLicense = true;

// if / else
if (age >= 18) {
  console.log("Adult");
} else {
  console.log("Minor");
}

// Ternary operator
const status = (age >= 18) ? "Adult" : "Minor";

// Logical operators
// && (AND) - both conditions must be true
if (age >= 18 && hasLicense) {
  console.log("Can drive");
}

// || (OR) - at least one condition must be true
if (age < 18 || !hasLicense) {
  console.log("Cannot drive");
}

// ! (NOT) - inverts a boolean
const isMinor = !(age >= 18);
console.log(isMinor); // false

// Comparison operators
console.log(5 > 3);   // true
console.log(5 < 3);   // false
console.log(5 >= 5);  // true
console.log(5 <= 4);  // false
console.log(5 === 5); // true (strict equality)
console.log(5 !== 3); // true (strict inequality)
console.log(5 == "5");  // true (loose - avoid!)
console.log(5 === "5"); // false (strict - prefer!)

Loops

const arr = [1, 2, 3];

// classic for
for (let i = 0; i < 10; i++) {
  console.log(i);
}

// for...of (values)
for (const v of arr) {
  console.log(v);
}

// for...in (keys/indices) - better for objects
const obj = { a: 1, b: 2 };
for (const key in obj) {
  console.log(key, obj[key]);
}

// while - checks condition first
let n = 3;
while (n > 0) {
  console.log(n--);
}

// do...while - executes at least once, then checks
let m = 0;
do {
  console.log("Runs at least once:", m);
  m++;
} while (m < 3);

Functions

โšก Function Types at a Glance

๐Ÿ“œ Declaration
function add(a, b) {
  return a + b;
}
โœ“ Hoisted Own this arguments
๐Ÿ“ Expression
const add = function(a, b) {
  return a + b;
};
โœ— Not hoisted Own this arguments
โžก๏ธ Arrow Function
const add = (a, b) => {
  return a + b;
};
โœ— Not hoisted Lexical this โœ— No arguments
โœจ Arrow Function Short Forms
Single param (no parens)
x => x * 2
Implicit return
(a, b) => a + b
Return object (wrap in parens)
(x, y) => ({ x, y })

Functions are reusable blocks of code. In JavaScript you create them either via a declaration (hoisted) or a function expression (created at runtime). The number of arguments you pass is flexible (you can pass fewer or more than the parameters declared), you can use default parameters, and traditional functions expose an arguments object. JavaScript has no true runtime overloads; you simulate them via optional/default/rest parameters and runtime checks. Function expressions can be named or anonymous.

Syntax: Declaration vs Expression

// Function declaration (hoisted)
function add(a, b) {
  return a + b;
}

// Function expression (created when this line runs)
const addExpr = function (a, b) {
  return a + b;
};

// Named function expression (better stack traces)
const addNamed = function addNamed(a, b) {
  return a + b;
};

Parameter flexibility (argument count is flexible)

JS doesnโ€™t enforce counts: missing parameters are undefined, extra arguments are ignored unless you read them.

function demo(a, b) {
  console.log('a:', a); // maybe undefined
  console.log('b:', b);
}
demo(1);           // a: 1, b: undefined
demo(1, 2, 3, 4);  // a: 1, b: 2 (extra args ignored unless used)

Default parameters

function greet(name = 'stranger', punctuation = '!') {
  return `Hello, ${name}${punctuation}`;
}
greet();              // "Hello, stranger!"
greet('Mehdi');       // "Hello, Mehdi!"
greet('Mehdi', '!!'); // "Hello, Mehdi!!"

// Defaults are evaluated at call time
function getId() { return Math.floor(Math.random() * 1000); }
function makeUser(id = getId(), role = 'reader') {
  return { id, role };
}

The arguments object (traditional functions)

Traditional (non-arrow) functions expose arguments, which contains all values passed. Prefer rest parameters for clarity.

function sumUsingArguments() {
  var total = 0;
  for (var i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}

// Prefer rest parameters
function sum(/* rest collects extra values */ ...nums) {
  var total = 0;
  for (var i = 0; i < nums.length; i++) {
    total += nums[i];
  }
  return total;
}

Rest parameters (...args)

Use rest parameters to collect any additional arguments passed to the function. Rules: only one rest parameter and it must be the last in the parameter list.

// Variadic sum
function sumAll(/* collects extra values */ ...nums) {
  var total = 0;
  for (var i = 0; i < nums.length; i++) {
    total += nums[i];
  }
  return total;
}
sumAll(1, 2, 3); // 6

// Mixing named parameters with rest (no array methods)
function log(label, /* any extra values */ ...values) {
  console.log('[' + label + ']', values); // values are just forwarded/printed
}
log('nums', 10, 20, 30);

// Traditional function using rest
function product(/* numbers */ ...nums) {
  var result = 1;
  for (var i = 0; i < nums.length; i++) {
    result *= nums[i];
  }
  return result;
}

// Invalid: rest must be the last parameter
// function badRest(...a, b) {}

// Note: We'll revisit the spread operator later when we cover arrays.

No true overloads in JavaScript

Declaring the same function name twice overwrites the previous definition. Simulate overloads with optional/default/rest parameters and runtime checks.

function format(value, options = {}) {
  if (typeof value === 'number') return value.toFixed(options.decimals ?? 2);
  if (typeof value === 'boolean') return value ? 'yes' : 'no';
  return String(value);
}

// TypeScript note: TS overloads are compile-time only; runtime is still one JS function.

Named vs Anonymous

// Declaration (named by syntax)
function hello() {}

// Anonymous function expression
const anon = function () {};

// Named function expression
const named = function doWork() {};

console.log(hello.name); // "hello"
console.log(anon.name);  // may be inferred by engines (e.g., "anon")
console.log(named.name); // "doWork"
Summary: Use traditional functions when you need your own this or arguments, or when defining constructors/methods. Use rest/default parameters for flexibility; avoid relying on implicit arguments where possible.

Arrow Functions

General syntax (at a glance)

Generic shape: (params) => { body }

// General forms
// (parameters) => expression
// (parameters) => { statements }

// Examples
const f1 = x => x * 2;            // one parameter, implicit return
const f2 = (a, b) => a + b;        // multiple parameters
const f3 = () => Date.now();       // zero parameters

// Block body: use an explicit return
const f4 = (x) => {
  const y = x * 2;
  return y;
};

// Returning an object literal: wrap in parentheses
const makePoint = (x, y) => ({ x, y });

Motivation: shorter syntax for inline callbacks and a lexical this (captures from surrounding scope), which avoids manual .bind in many cases. Arrow functions are always anonymous (engines may infer a name from the variable/property theyโ€™re assigned to).

Headsโ€‘up: Weโ€™ll dive deeper into how this and arguments behave in both traditional functions and arrow functions in the following sections.

Syntax variations & abbreviations

  • Parameter parentheses are optional only when there is exactly one parameter: x => x * 2. For zero or more than one parameter, you must use parentheses: () => ..., (a, b) => ....
  • Braces are optional only when the body is a single expression (no statements): x => x * 2. If you use a block body with braces { ... }, you write normal statements.
  • With a single-expression body (no braces), the result is returned implicitly. With a block body { ... }, use an explicit return for output.
// Single parameter, implicit return
const double = x => x * 2;

// Zero or multiple parameters need parentheses
const add = (a, b) => a + b;      // implicit return
const now = () => Date.now();     // implicit return

// Block body requires explicit return
const addVerbose = (a, b) => {
  const sum = a + b;
  return sum;
};

// Returning an object literal (wrap in parentheses)
const makePoint = (x, y) => ({ x, y });

// Defaults (rest covered above)
const greet = (name = 'stranger') => `Hello, ${name}!`;
const fullName = ({ first, last }) => `${first} ${last}`;

Always anonymous (with name inference)

const doThing = () => {};
console.log(doThing.name); // often "doThing" (inferred), but arrow syntax itself has no name slot

Key differences: this and arguments

Weโ€™ll cover how this and arguments behave in traditional functions vs arrow functions after we introduce objects. Jump there when ready: this & arguments: functions vs arrows.

Examples in practice

// Event listener
document.querySelector('button').addEventListener('click', (evt) => {
  console.log('Clicked at', evt.clientX, evt.clientY);
});

// Timers
setTimeout(() => console.log('Hello after 1s'), 1000);

// Simple math helpers
const square = x => x * x;
const add = (a, b) => a + b;

// Returning objects succinctly
const makePoint = (x, y) => ({ x, y });
Choose wisely: Use arrows for concise callbacks and when lexical this helps; use traditional functions for methods/constructors or when you need your own this/arguments.

Callbacks

๐Ÿ”„ What is a Callback?

๐Ÿ“ฆ
Your Function
compute(a, b, cb)
โ†’
โš™๏ธ
Does Work
result = a + b
โ†’
๐Ÿ“ž
Calls Back
cb(result)
๐Ÿ’ก Key Concept
A callback is a function you pass as an argument. The receiving function calls it later with results. This lets YOU control what happens with the data!
โŒ Hardcoded Behavior
function compute(a, b) {
  console.log(a + b); // Fixed!
}
Can't customize output
โœ… With Callback
function compute(a, b, cb) {
  cb(a + b); // You decide!
}
Caller controls behavior

A callback is a function passed as an argument to customize behaviour (e.g., what to do with a result).

function compute(a, b, render) {
  const res = a + b; // internal work
  render(res);       // delegate final action to the callback
}

compute(2, 3, x => console.log("Result:", x));

// async example
setTimeout(() => console.log("Hello after 1s"), 1000);

Why callbacks? Decoupling behaviour from logic

Consider a small โ€œguess gameโ€ where the function compares your number to a random one and prints a message. If the function prints inside, the behaviour is hardcoded and cannot be changed by the caller.

Problem โ€” hardcoded behaviour
function randomNumber() {
  // random integer in [1, 5]
  return Math.floor(Math.random() * 5) + 1;
}

function devinette(nbr) {
  const aleatoire = randomNumber();
  if (aleatoire === nbr) {
    console.log("bravo");
  } else {
    console.log("echec");
  }
}

devinette(3);
Limitation: The author of devinette decided the success/failure behaviour. The caller cannot log differently, update UI, count attempts, or do anything else.
Solution โ€” pass callbacks to choose behaviours

Let the function focus on logic (checking the guess) and let the caller choose what to do on success and on failure.

function randomNumber() {
  return Math.floor(Math.random() * 5) + 1;
}

function devinette(nbr, onSuccess, onFailure) {
  const secret = randomNumber();
  if (secret === nbr) {
    onSuccess(secret, nbr);
  } else {
    onFailure(secret, nbr);
  }
}

// Caller decides behaviours
function celebrate(secret, guess) {
  console.log("๐ŸŽ‰ bravo โ€” secret:", secret, "guess:", guess);
}
function report(secret, guess) {
  console.log("โŒ echec โ€” secret:", secret, "guess:", guess);
}

devinette(3, celebrate, report);
devinette(2, (secret, guess) => console.log("Nice!", secret, guess), (secret, guess) => console.log("Try again.", secret, guess));
Benefit: The game logic doesnโ€™t know about printing, UI, or storage. The caller controls sideโ€‘effects, which makes the function reusable.
Variant โ€” single callback that receives the result

Another pattern is to return a result object (or boolean) via one callback and let the caller branch.

function randomNumber() {
  return Math.floor(Math.random() * 5) + 1;
}

function devinette(nbr, done) {
  const secret = randomNumber();
  const success = secret === nbr;
  done({ success, secret, guess: nbr });
}

devinette(4, (r) => {
  if (r.success) {
    console.log("bravo โ€”", r);
  } else {
    console.log("echec โ€”", r);
  }
});

Objects

๐Ÿ—๏ธ Ways to Create Objects

{ } Object Literal MOST COMMON
const user = {
  name: "Mehdi",
  hello() { console.log(this.name); }
};
๐Ÿ”ง Constructor Function
function Person(name) {
  this.name = name;
}
const p = new Person("Sara");
๐Ÿ“ฆ Class (ES2015+) MODERN
class Person {
  constructor(name) { this.name = name; }
  hello() { console.log(this.name); }
}
๐Ÿ“ Object Operations
obj.prop = value // add/set delete obj.prop // remove "prop" in obj // check

Ways to create objects: literal, function constructor, class (ES2015+).

Literal & dynamic properties
// object literal with properties and a method
const user = {
  id: 1,
  name: "Mehdi",
  hello: function () { // method using 'this'
    console.log(`Hi, I'm ${this.name}`);
  }
};

// add properties dynamically
user.age = 32;

// call existing method
user.hello(); // Hi, I'm Mehdi

// add a method dynamically
user.rename = function (newName) {
  this.name = newName;
};
user.rename("Sara");
user.hello(); // Hi, I'm Sara

// remove a property
delete user.id;

// membership check
console.log("name" in user); // true
Function constructor (simple)
function Person(name) {
  this.name = name;
  this.hello = function () {
    console.log(`Hi, I'm ${this.name}`);
  };
}

const p = new Person("Sara");
p.hello();
Encapsulation (public vs private)
We can mimic private data by using a local let variable inside the constructor (captured by closure) and expose only the methods we want via this. Properties on this are public.
function Counter(start) {
  // private (not accessible from the instance)
  let count = (typeof start === 'number') ? start : 0;

  // public methods (privileged: they can see 'count')
  this.get = function () { return count; };
  this.inc = function () { count++; };
  this.reset = function () { count = 0; };
}

const c = new Counter(5);
console.log(c.count);      // undefined (private)
console.log(c.get());      // 5
c.inc();
console.log(c.get());      // 6
c.reset();
console.log(c.get());      // 0
Class (ES2015+)
class Person {
  constructor(name) { this.name = name; }
  hello() { console.log(`Hi, I'm ${this.name}`); }
}
const q = new Person("Youssef");
q.hello();

this and arguments

๐ŸŽฏ How this Works

๐Ÿ“œ Regular Function
โšก Dynamic this
Depends on how it's called
obj.method() โ†’ this = obj func() โ†’ this = undefined/window
โžก๏ธ Arrow Function
๐Ÿ”’ Lexical this
Inherits from where defined
Always uses outer this Perfect for callbacks!
Regular Function
arguments โœ“
Arrow Function
arguments โœ—
Use ...rest instead
Arrow vs function: Arrow functions do not have their own this; they capture the outer this. Classic function has a dynamic this depending on how itโ€™s called.
this with function vs arrow in callbacks
const obj = {
  value: 42,
  withFunction() {
    setTimeout(function() {
      console.log('function this.value =', this && this.value); // undefined in browsers (this === window)
    }, 0);
  },
  withArrow() {
    setTimeout(() => {
      console.log('arrow this.value =', this.value); // 42 (inherits obj as this)
    }, 0);
  }
};
obj.withFunction();
obj.withArrow();
arguments object: Inside nonโ€‘arrow functions you get a special arrayโ€‘like object named arguments. Prefer modern rest ...args to get a real array.
Using arguments vs using rest ...args
function demo() {
  console.log('arguments length =', arguments.length);
  console.log('first arg =', arguments[0]);
  const arr = Array.from(arguments);
  console.log('as real array =', arr.join(', '));
}
demo(1, 'a', true);

function better(...args) {
  console.log('args is a real array:', Array.isArray(args), args);
}
better(1, 2, 3);

Rest & Spread Operators (...)

๐Ÿ”„ Same Syntax, Opposite Directions

๐Ÿ“ฅ REST (Gather)
1 2 3
โ†“
[1, 2, 3]
function sum(...nums) { }
Multiple items โ†’ One array
๐Ÿ“ค SPREAD (Scatter)
[1, 2, 3]
โ†“
1 2 3
Math.max(...arr)
One array โ†’ Multiple items
๐Ÿ“ Quick Reference
REST - Parameters
fn(a, ...rest)
REST - Destructure
[a, ...rest] = arr
SPREAD - Arrays
[...arr1, ...arr2]
SPREAD - Objects
{...obj, newProp}

The ... syntax serves two opposite purposes depending on context. Understanding the difference is crucial:

Key Concept:
  • REST (gather): Collects multiple items INTO one array/object
  • SPREAD (scatter): Expands one array/object INTO multiple items

Same syntax ... but opposite directions!

REST: Collecting Multiple โ†’ One

// REST in function parameters: collect arguments into array
function sum(...numbers) {  // Gathers all arguments
  return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// numbers = [1, 2, 3, 4] - collected into array

// REST with named parameters (rest must be last!)
function greet(greeting, ...names) {
  return `${greeting} ${names.join(', ')}!`;
}
console.log(greet('Hello', 'Ali', 'Sara', 'Omar'));
// "Hello Ali, Sara, Omar!"

// REST in array destructuring: collect remaining items
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first);  // 1
console.log(second); // 2
console.log(rest);   // [3, 4, 5] - collected remaining

// REST in object destructuring: collect remaining properties
const user = { name: 'Mehdi', age: 32, city: 'Casa', role: 'dev' };
const { name, ...otherProps } = user;
console.log(name);       // "Mehdi"
console.log(otherProps); // { age: 32, city: 'Casa', role: 'dev' }

SPREAD: Expanding One โ†’ Multiple

// SPREAD in arrays: expand array into individual elements
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// Combine arrays
const combined = [...arr1, ...arr2];  // Spreads both arrays
console.log(combined); // [1, 2, 3, 4, 5, 6]

// Clone array (shallow copy)
const clone = [...arr1];
console.log(clone); // [1, 2, 3]

// SPREAD in function calls: expand array into arguments
const nums = [1, 5, 3, 9, 2];
console.log(Math.max(...nums)); // 9
// Equivalent to: Math.max(1, 5, 3, 9, 2)

// SPREAD in objects: expand object into properties
const user = { name: "Mehdi", age: 32 };
const location = { city: "Casablanca", country: "Morocco" };

// Merge objects
const profile = { ...user, ...location };  // Spreads both objects
console.log(profile);
// { name: "Mehdi", age: 32, city: "Casablanca", country: "Morocco" }

// Override properties (later values win)
const updated = { ...user, age: 33, role: "admin" };
console.log(updated);
// { name: "Mehdi", age: 33, role: "admin" }

Putting It Together: REST + SPREAD

// Use both in same function!
function mergeAndLog(label, ...arrays) {  // REST: collect arrays
  const merged = [].concat(...arrays);    // SPREAD: expand each array
  console.log(`${label}:`, merged);
  return merged;
}

mergeAndLog('Numbers', [1, 2], [3, 4], [5, 6]);
// "Numbers: [1, 2, 3, 4, 5, 6]"

// Practical example: updating nested objects immutably
const state = {
  user: { name: 'Ali', age: 25 },
  settings: { theme: 'dark', lang: 'en' }
};

// Update user age without mutating original
const newState = {
  ...state,                          // SPREAD: copy all properties
  user: {
    ...state.user,                   // SPREAD: copy user properties
    age: 26                          // Override age
  }
};
console.log(state.user.age);      // 25 (original unchanged)
console.log(newState.user.age);   // 26 (new state updated)
Common Mistake:
// โŒ WRONG: Can't use rest in the middle
function bad(...first, last) { }  // SyntaxError!

// โœ… CORRECT: Rest must be last parameter
function good(first, ...rest) { }

// โŒ WRONG: Can't spread into nothing
const x = ...arr;  // SyntaxError!

// โœ… CORRECT: Spread inside array/object/call
const x = [...arr];
const y = Math.max(...arr);

Template Literals

Template literals use backticks (`) and provide a cleaner, more readable way to work with strings compared to concatenation.

โœจ Template Literal Syntax

โŒ Concatenation (Old)
'Hello ' + name + '!'
โœ… Template Literal (Modern)
\`Hello \${name}!\`
๐Ÿ“
Multi-line Strings
No \\n needed
โšก
Expression Interpolation
\${expression}
๐Ÿท๏ธ
Tagged Templates
tag\`string\`

Why Template Literals? The Problem with Concatenation

const user = { name: 'Mehdi', email: 'mehdi@example.com' };
const orderCount = 5;
const total = 249.99;

// โŒ CONCATENATION: Hard to read, easy to make mistakes
const msg1 = 'Hello ' + user.name + ',\n' +
           'You have ' + orderCount + ' orders.\n' +
           'Total: $' + total.toFixed(2) + '\n' +
           'Email: ' + user.email;

// โœ… TEMPLATE LITERAL: Clean, readable, maintainable
const msg2 = `Hello ${user.name},
You have ${orderCount} orders.
Total: $${total.toFixed(2)}
Email: ${user.email}`;

console.log(msg2);

// Spot the difference:
// - No + operators cluttering the code
// - Multi-line strings work naturally (no \n needed)
// - Easy to see the structure
// - Expressions inside ${} are clear

Basic Features

const name = "Mehdi";
const age = 32;

// Expression interpolation
const greeting = `Hello, ${name}! You are ${age} years old.`;
console.log(greeting);

// Any expression works
const price = 100;
const tax = 0.2;
const total = `Total: ${price * (1 + tax)} MAD`;
console.log(total); // Total: 120 MAD

// Function calls
const loud = (text) => text.toUpperCase();
console.log(`Message: ${loud('hello')}`); // Message: HELLO

// Conditional expressions
const status = age >= 18 ? 'adult' : 'minor';
const info = `${name} is an ${status}`;
console.log(info); // Mehdi is an adult

Practical Use Cases

// Use Case 1: HTML Generation
function createUserCard(user) {
  return `
    

${user.name}

Email: ${user.email}

${user.active ? 'โ— Active' : 'โ—‹ Inactive'}
`; } const html = createUserCard({ name: 'Sara', email: 'sara@example.com', active: true }); console.log(html); // Use Case 2: SQL Queries (be careful with injection!) function buildQuery(table, conditions) { const where = conditions.map(c => `${c.field} = '${c.value}'`).join(' AND '); return `SELECT * FROM ${table} WHERE ${where}`; } const query = buildQuery('users', [ { field: 'age', value: 25 }, { field: 'city', value: 'Casa' } ]); console.log(query); // Use Case 3: URLs and API endpoints const userId = 123; const apiUrl = `https://api.example.com/users/${userId}/posts?limit=10`; console.log(apiUrl); // Use Case 4: Log messages with context function logAction(action, user, timestamp = Date.now()) { console.log(`[${new Date(timestamp).toISOString()}] ${user.name} performed: ${action}`); } logAction('login', { name: 'Ali' });
When to use each:
  • Template literals: Multi-line strings, complex interpolation, HTML/SQL generation
  • Concatenation: Simple single additions like 'Hello' + name
  • General rule: If you need more than 2 values or multiple lines โ†’ use templates

Destructuring (Objects & Arrays)

Destructuring lets you extract values from objects or arrays into distinct variables using a clean, declarative syntax. It's a shorthand for pulling out what you need.

๐Ÿงฉ Destructuring Patterns

๐Ÿ“ฆ Object Destructuring
// Source object
{ name: "Mehdi", age: 32 }
โ†“ extract by key name
const { name, age } = obj;
// name โ†’ "Mehdi", age โ†’ 32
๐Ÿ“š Array Destructuring
// Source array
[ "red", "green", "blue" ]
โ†“ extract by position
const [ first, second ] = arr;
// first โ†’ "red", second โ†’ "green"
{ name: alias } { x = default } { ...rest } [ , second ]

Object Destructuring

Extract properties from an object by matching variable names to property keys.

Basic object destructuring
const user = {
  name: "Mehdi",
  age: 32,
  city: "Casablanca"
};

// Extract properties into variables
const { name, age } = user;
console.log(name); // "Mehdi"
console.log(age);  // 32

// Rename while destructuring
const { name: userName, city: location } = user;
console.log(userName); // "Mehdi"
console.log(location); // "Casablanca"

// Default values (if property doesn't exist)
const { country = "Morocco", email = "none" } = user;
console.log(country); // "Morocco"
console.log(email);   // "none"
Function parameters destructuring
// Before: accessing object properties manually
function displayUserOld(user) {
  console.log(`Name: ${user.name}, Age: ${user.age}`);
}

// After: destructure in the parameter list
function displayUser({ name, age, city = "Unknown" }) {
  console.log(`Name: ${name}, Age: ${age}, City: ${city}`);
}

const user = { name: "Youssef", age: 28 };
displayUser(user); // Name: Youssef, Age: 28, City: Unknown

// REST in destructuring: collect remaining properties
const { name, ...rest } = { name: "Ali", age: 25, city: "Fes" };
console.log(name); // "Ali"
console.log(rest); // { age: 25, city: "Fes" }

Array Destructuring

Extract values from arrays by position rather than by name.

Basic array destructuring
const colors = ["red", "green", "blue", "yellow"];

// Extract by position
const [first, second] = colors;
console.log(first);  // "red"
console.log(second); // "green"

// Skip elements using commas
const [, , third] = colors;
console.log(third); // "blue"

// Default values
const [a, b, c, d, e = "default"] = colors;
console.log(e); // "default" (no 5th element)

// Swapping variables without temp variable
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1
REST in array destructuring
const numbers = [1, 2, 3, 4, 5];

// Capture first element and rest
const [first, ...rest] = numbers;
console.log(first); // 1
console.log(rest);  // [2, 3, 4, 5]

// Function returning multiple values
function getCoordinates() {
  return [10, 20];
}
const [x, y] = getCoordinates();
console.log(x, y); // 10 20

// Destructure in loop
const pairs = [[1, 2], [3, 4], [5, 6]];
pairs.forEach(([a, b]) => {
  console.log(`a=${a}, b=${b}`);
});
Mixed: destructuring arrays of objects
const users = [
  { id: 1, name: "Fatima", role: "admin" },
  { id: 2, name: "Hassan", role: "user" },
  { id: 3, name: "Nadia", role: "moderator" }
];

// Destructure first user's properties
const [{ name: firstName, role }] = users;
console.log(firstName); // "Fatima"
console.log(role);      // "admin"

// Map with destructuring
const names = users.map(({ name }) => name);
console.log(names); // ["Fatima", "Hassan", "Nadia"]

// Filter admins using destructuring
const admins = users.filter(({ role }) => role === "admin");
console.log(admins); // [{ id: 1, name: "Fatima", role: "admin" }]
When to use destructuring:
  • Function parameters with many options (pass an object, destructure)
  • Extracting data from API responses
  • Working with React props/state
  • Swapping variables
  • Cleaning up code that repeatedly accesses obj.property

Arrays

๐Ÿ“‹ Array Methods Decision Guide

๐Ÿ”„
.map()
Transform each item
๐Ÿ”
.filter()
Select some items
โž•
.reduce()
Combine to one value
๐ŸŽฏ
.find()
Find one item
โ“
.some()
Any match?
โœ…
.every()
All match?
โš ๏ธ
.sort() โ€” mutates original array! Use [...arr].sort() to preserve original.

In JS, arrays are objects with flexible size (no fixed length required).

const t = [10, 20];
t.push(30);     // [10, 20, 30]
t[10] = 99;     // holes allowed; length becomes 11
console.log(t.length);

Array Methods with Callbacks

NB: Methods like forEach, map, filter, sort (and others) accept callbacks. They abstract away the โ€œhow to iterateโ€ (and in the case of sort, the sorting logic) so you focus on what to do. This is the practical power of callbacks.

๐Ÿ”ง Array Methods at a Glance

.map()
[1,2,3] โ†’ [2,4,6]
Transform each item
.filter()
[1,2,3,4] โ†’ [2,4]
Keep matching items
.reduce()
[1,2,3] โ†’ 6
Combine into one value
.find()
[1,2,3] โ†’ 2
First matching item
.some()
[1,2,3] โ†’ true
Any match? (boolean)
.every()
[1,2,3] โ†’ false
All match? (boolean)
๐Ÿ”„ Returns new array ๐Ÿ“Š Returns single value ๐Ÿ” Returns item/undefined โœ“/โœ— Returns boolean

โšก Quick Decision Guide: Which Method to Use?

// Decision Guide:
// Transform each item โ†’ .map()
// Select some items โ†’ .filter()
// Combine to single value โ†’ .reduce()
// Find one item โ†’ .find()
// Check if any match โ†’ .some()
// Check if all match โ†’ .every()

const orders = [
  { id: 1, total: 50, status: 'paid' },
  { id: 2, total: 120, status: 'pending' },
  { id: 3, total: 80, status: 'paid' }
];

// Transform โ†’ .map()
const ids = orders.map(o => o.id);
console.log('IDs:', ids); // [1, 2, 3]

// Select โ†’ .filter()
const paid = orders.filter(o => o.status === 'paid');
console.log('Paid:', paid.length); // 2

// Combine โ†’ .reduce()
const revenue = paid.reduce((sum, o) => sum + o.total, 0);
console.log('Revenue:', revenue); // 130

// Find โ†’ .find()
const order = orders.find(o => o.id === 2);
console.log('Found:', order?.total); // 120

// Check any โ†’ .some()
const hasLarge = orders.some(o => o.total > 100);
console.log('Has large:', hasLarge); // true

// Check all โ†’ .every()
const allPaid = orders.every(o => o.status === 'paid');
console.log('All paid:', allPaid); // false

Why these methods? Compare with traditional loops

Before these helpers, you wrote and repeated a lot of boilerplate: create variables, loop with indexes, push into new arrays, etc.

Traditional approach โ€” more code to manage
const nums = [1, 2, 3, 4];

  // 1) Sum
  let sum = 0;
  for (let i = 0; i < nums.length; i++) {
    sum += nums[i];
  }
  console.log('sum =', sum);

// 2) Transform (squares) โ†’ build a new array โ†’ build a new array
const squares = [];
for (let i = 0; i < nums.length; i++) {
  squares.push(nums[i] * nums[i]);
}
console.log('squares =', squares);

// 3) Filter evens โ†’ build a new array conditionally โ†’ build a new array conditionally
const evens = [];
for (let i = 0; i < nums.length; i++) {
  if (nums[i] % 2 === 0) evens.push(nums[i]);
}
console.log('evens =', evens);
Nothing wrong with loopsโ€”but itโ€™s easy to make mistakes (off-by-one, forgetting to create result arrays, mixing concerns). Array methods let you express intent directly.

forEach โ€” perform side effects

const nums = [1, 2, 3];
let sum = 0;
nums.forEach((value, index, array) => {
  sum += value;
});
console.log(sum); // 6

map โ€” transform into a new array

const celsius = [0, 20, 30];
const fahrenheit = celsius.map(c => c * 9/5 + 32);
console.log(fahrenheit); // [32, 68, 86]

// map objects
const users = [{id:1,name:"A"}, {id:2,name:"B"}];
const names = users.map(u => u.name);

filter โ€” keep items that match a predicate

const nums = [1, 2, 3, 4, 5, 6];
const evens = nums.filter(n => n % 2 === 0); // [2,4,6]

// filter objects
const people = [
  { name: "Sara", age: 22 },
  { name: "Ali", age: 17 },
];
const adults = people.filter(p => p.age >= 18);

sort โ€” order items (mutates the array)

Heads up: sort changes the original array. Copy first if you need to keep the original: const sorted = [...arr].sort(...)
const nums = [10, 2, 5];
// default is lexicographic by string
console.log([...nums].sort());       // [10,2,5] โ†’ ["10","2","5"] โ†’ [10,2,5]

// numeric ascending / descending
console.log([...nums].sort((a,b) => a - b)); // [2,5,10]
console.log([...nums].sort((a,b) => b - a)); // [10,5,2]

// sort strings (case-insensitive)
const names = ["zara","Ali","mehdi"];
console.log([...names].sort((a,b) => a.localeCompare(b, undefined, { sensitivity: "base" })));

// sort objects by field
const people = [{name:"Sara",age:22},{name:"Ali",age:30}];
people.sort((p,q) => p.age - q.age); // by age asc (mutates)

More useful ones

// find / findIndex
const arr = [5, 12, 8, 130, 44];
const firstBig = arr.find(n => n > 10);      // 12
const idxBig   = arr.findIndex(n => n > 10); // 1

// some / every
[1, 3, 5].some(n => n % 2 === 0);  // false
[2, 4, 6].every(n => n % 2 === 0); // true

// reduce (sum)
const total = [1,2,3,4].reduce((acc, n) => acc + n, 0); // 10

Error Handling with try / catch

Use try { ... } catch (err) { ... } finally { ... } to handle exceptions and keep your program in control. You can also throw your own errors when inputs are invalid.

๐Ÿ›ก๏ธ Error Handling Flow

try { }
Code that might fail
โœ“
Success
โœ—
Error
Continue
catch { }
โ†“
finally { }
Always runs (optional)
TypeError RangeError SyntaxError ReferenceError

Basic try/catch

const text = '{"id":1,"name":"Mehdi"}';
try {
  const obj = JSON.parse(text);
  console.log('Parsed:', obj.name);
} catch (err) {
  console.error('Invalid JSON:', err.message);
}

Throwing your own errors

function validateAge(age) {
  if (typeof age !== 'number' || Number.isNaN(age)) {
    throw new TypeError('age must be a number');
  }
  if (age < 0) throw new RangeError('age must be >= 0');
  return age;
}

try {
  const a = validateAge(20);
  console.log('ok:', a);
  validateAge('x'); // will throw
} catch (e) {
  console.error('Validation error:', e.message);
}

finally for cleanup

function work() {
  console.log('start');
  try {
    // do stuff that may throw
    JSON.parse('{ bad json');
  } catch (e) {
    console.error('caught:', e.message);
  } finally {
    console.log('cleanup always runs');
  }
  console.log('end');
}
work();
Headsโ€‘up: try/catch only catches errors that happen inside the try block during that call. Errors thrown later (e.g., in a setTimeout callback) must be caught inside that callback, or handled via Promises/async/await (next section).
Asynchronous means work is scheduled to run later (timers, network, I/O); your current function returns and the callback continues on a future turn of the event loop.
try {
  setTimeout(() => { throw new Error('async boom'); }, 0);
} catch (e) {
  console.log('This will NOT catch the async error');
}

// Fix: catch inside the callback
setTimeout(() => {
  try {
    throw new Error('async boom');
  } catch (e) {
    console.log('Caught later:', e.message);
  }
}, 0);

Error levels: bubbling to the nearest catch

When an error occurs, JavaScript looks for the nearest surrounding try/catch up the call stack. If none is found, it reaches the global error handler and can halt your program. Catch at the appropriate boundary to avoid app crashes.

Example โ€” caught at top level
function c() {
  throw new Error('boom in c');
}
function b() {
  c(); // no catch here
}
function a() {
  try {
    b();
    console.log('after b'); // not reached
  } catch (e) {
    console.log('caught in a:', e.message);
  }
}
a();
Example โ€” caught closer to the source
function c() {
  throw new Error('boom in c');
}
function b() {
  try {
    c();
  } catch (e) {
    console.log('handled in b:', e.message);
  }
}
function a() {
  b();
  console.log('a continues'); // reached
}
a();
Uncaught errors: If nothing catches an error, it becomes uncaught and the global handler runs (browser console shows an uncaught error; servers may terminate the process). Prefer catching at module/function boundaries, logging context, and (when appropriate) reโ€‘throwing a more descriptive error.

โšก Where to Place try/catch: Practical Guidance

Understanding error mechanics is one thing - knowing where to catch errors is another. Here's how to think about it:

// โŒ BAD: Catching too granularly (too many try/catch)
function processUserBad(data) {
  let parsed;
  try {
    parsed = JSON.parse(data);
  } catch (e) {
    console.error('Parse error:', e.message);
    return null;
  }
  
  try {
    validate(parsed);
  } catch (e) {
    console.error('Validation error:', e.message);
    return null;
  }
  
  try {
    save(parsed);
  } catch (e) {
    console.error('Save error:', e.message);
    return null;
  }
  
  return parsed;
}

// โœ… GOOD: Catch at function boundary, handle meaningfully
function processUserGood(data) {
  try {
    const parsed = JSON.parse(data);
    validate(parsed);
    save(parsed);
    return { success: true, data: parsed };
  } catch (error) {
    console.error('Failed to process user:', error.message);
    // Log context for debugging
    console.error('Input data:', data);
    return { success: false, error: error.message };
  }
}

// ๐Ÿ“ Helper functions
function validate(user) {
  if (!user.email) throw new Error('Email required');
  if (!user.name) throw new Error('Name required');
}

function save(user) {
  // Simulate DB save that might fail
  if (Math.random() < 0.1) throw new Error('Database error');
  console.log('Saved:', user.name);
}

// Test it
const result1 = processUserGood('{"name":"Ali","email":"ali@test.com"}');
console.log('Result:', result1);

const result2 = processUserGood('invalid json');
console.log('Result:', result2);

๐ŸŽฏ Decision Guide: Where to Catch Errors

Three levels of error handling:
  1. Function boundary: Catch at the top level of your function if it's public/exported
  2. Module boundary: Catch when crossing module boundaries (API routes, event handlers)
  3. Application boundary: Global error handler as last resort
// Level 1: Function boundary (pure function)
function calculateTotal(items) {
  try {
    return items.reduce((sum, item) => sum + item.price, 0);
  } catch (error) {
    // Log and return safe default
    console.error('Calculation error:', error.message);
    return 0;
  }
}

// Level 2: Module boundary (API route handler)
function handleUserRequest(req, res) {
  try {
    const user = parseUser(req.body);
    const saved = saveToDatabase(user);
    res.json({ success: true, user: saved });
  } catch (error) {
    // Caught at boundary, log context, send user-friendly response
    console.error('[API Error]', error.message, { body: req.body });
    res.status(400).json({
      success: false,
      error: 'Failed to create user',
      details: error.message
    });
  }
}

// Level 3: Global error handler (last resort)
window.addEventListener('error', (event) => {
  console.error('[Global Error]', event.error);
  // Send to error tracking service
  // Show user-friendly message
});

// Don't catch too early - let errors bubble to the right level!

Promises & Asynchronous Programming

Motivation: As programs grow, nesting callbacks leads to callback hell (a โ€œpyramidโ€ of indentation) and scattered error handling. Promises give a standard way to represent an async result (pending โ†’ settled), compose steps, and handle errors in one place. async/await is syntax on top of promises for a synchronous style.

๐Ÿ’ก Key Insight โ€” The Power of Promises: When a function returns a Promise, you (the caller) decide how to consume it:
  • Use .then() for async callback-style (non-blocking)
  • Use await for sync-like sequential code (easier to read)
This flexibility is what makes Promises powerful โ€” the API doesn't force a style on you!

Callback hell (the pyramid)

The problem is most visible when each API expects a callback, and inside your callback you must call the next API that also expects a callback, and so onโ€ฆ leading to deep nesting and duplicated error checks.

Dependency chain: In the real flow below โ€” fetch users โ†’ user details โ†’ user posts โ†’ comments โ€” each step depends on the previous stepโ€™s result, so you canโ€™t start the next step until the prior callback finishes. This forces nesting with plain callbacks; promises/asyncโ€‘await flatten the flow while preserving order.
// Real example: fetch users โ†’ user details โ†’ user posts โ†’ comments
  // Each API expects a callback: (err, data)
  function fetchUsers(cb){
    setTimeout(function(){ cb(null, [ {id:1,name:'A'}, {id:2,name:'B'} ]); }, 30);
  }
  function fetchUserDetails(userId, cb){
    setTimeout(function(){ cb(null, { id:userId, email:'a@example.com' }); }, 30);
  }
  function fetchUserPosts(userId, cb){
    setTimeout(function(){ cb(null, [ { id:10, userId, title:'Hello' } ]); }, 30);
  }
  function fetchPostComments(postId, cb){
    setTimeout(function(){ cb(null, [ { id:100, postId, text:'Nice!' } ]); }, 30);
  }

  // Pyramid of doom: callback inside callback inside callbackโ€ฆ
  fetchUsers(function (err, users) {
    if (err) return console.error('users error:', err.message || err);
    const user = users[0];
    fetchUserDetails(user.id, function (err, details) {
      if (err) return console.error('details error:', err.message || err);
      fetchUserPosts(user.id, function (err, posts) {
        if (err) return console.error('posts error:', err.message || err);
        const post = posts[0];
        fetchPostComments(post.id, function (err, comments) {
          if (err) return console.error('comments error:', err.message || err);
          console.log('done (callbacks):', { user, details, post, comments });
        });
      });
    });
  });

From callbacks to promises

// Promise that resolves after a delay
  function delay(ms) {
    return new Promise((resolve) => {
      setTimeout(() => resolve(ms), ms);
    });
  }

  delay(200).then((v) => {
    console.log('resolved after', v, 'ms');
  });

  // Real-life: users โ†’ details (promises)
  function fetchUsersP(){
    return new Promise((resolve) => setTimeout(() => resolve([
      {id:1,name:'A'}, {id:2,name:'B'}
    ]), 30));
  }
  function fetchDetailsP(id){
    return new Promise((resolve) => setTimeout(() => resolve({ userId:id, score:id*10 }), 30));
  }

  fetchUsersP()
  .then(users => Promise.all(users.map(u =>
    fetchDetailsP(u.id).then(d => ({ id:u.id, name:u.name, details:d }))
  )))
  .then(all => console.log('all done (promises):', all))
  .catch(err => console.error('error:', err.message));

Chaining and returning

function step(n) {
  return new Promise((resolve) => setTimeout(() => resolve(n), 50));
}

step(1)
  .then(v => { console.log('step', v); return step(v + 1); })
  .then(v => { console.log('step', v); return step(v + 1); })
  .then(v => { console.log('step', v); })
  .catch(err => console.error('caught:', err));

Error propagation

new Promise((resolve, reject) => {
  reject(new Error('boom'));
})
  .then(() => console.log('will not run'))
  .catch(err => console.error('handled once:', err.message));

Select a scenario to begin

Choose a promise scenario above to see how it flows through different states.

Step 0/0

Click a scenario button above to begin

Promise Lifecycle Interactive Diagram Pending Initial state async operation Fulfilled โœ“ Success has value Rejected โœ— Error has reason .then() handles success returns new Promise .catch() handles error returns new Promise .finally() cleanup handler always runs resolve(value) reject(reason) value โ†’ error โ†’ error โ†“ new Promise โŸฒ finally โ†“ finally โ†‘ โฑ after 1s โฑ after 1s
Promise lifecycle: pending โ†’ settled (fulfilled/rejected). Choose a scenario above to see the interactive animation.

Promise Chaining Scenarios

Each button above demonstrates a different promise flow pattern. Here are the code examples:

1. Resolve: Basic Success Flow
// Promise resolves โ†’ .then() handles the value
const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Success! Data loaded'), 1000);
});

promise
  .then(value => {
    console.log(value); // "Success! Data loaded"
    return value.toUpperCase();
  })
  .then(upperValue => {
    console.log(upperValue); // "SUCCESS! DATA LOADED"
  });
2. Reject: Error Handling Flow
// Promise rejects โ†’ .catch() handles the error
const promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('Network timeout')), 1000);
});

promise
  .then(value => {
    console.log('This will not run');
  })
  .catch(error => {
    console.error('Caught:', error.message); // "Caught: Network timeout"
    return 'Recovered with default value';
  })
  .then(recovered => {
    console.log(recovered); // "Recovered with default value"
  });
3. Chain: .then() Throws Error
// .then() receives value but throws an error
const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve({ id: 42, name: 'Alice' }), 500);
});

promise
  .then(user => {
    console.log('User received:', user); // {id: 42, name: "Alice"}
    
    // Validate user ID and throw error if invalid
    if (user.id < 100) {
      throw new Error('User ID too low');
    }
    return user;
  })
  .catch(error => {
    console.error('Error caught:', error.message);
    // "Error caught: User ID too low"
  });
4. Chain Success: .then() Returns Another Promise
// .then() returns a new promise that resolves - promise chaining
function fetchUser(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id, name: 'Bob' }), 300);
  });
}

function fetchUserPosts(userId) {
  return new Promise(resolve => {
    setTimeout(() => resolve(['Post 1', 'Post 2']), 400);
  });
}

fetchUser(123)
  .then(user => {
    console.log('Got user:', user); // {id: 123, name: "Bob"}
    // Return another promise - chain waits for it to resolve
    return fetchUserPosts(user.id);
  })
  .then(posts => {
    console.log('Got posts:', posts); // ["Post 1", "Post 2"]
  });
5. Finally: Cleanup Handler (Always Runs)
// .finally() runs regardless of success or failure
let isLoading = true;

function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Data loaded');
    }, 500);
  });
}

fetchData()
  .then(data => {
    console.log('Success:', data); // "Success: Data loaded"
  })
  .catch(error => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    isLoading = false;
    console.log('Cleanup: isLoading =', isLoading); // false
    console.log('This always runs!'); // Always runs
  });

Async/await: Modern Asynchronous JavaScript

Async/await is syntactic sugar over promises that makes asynchronous code look and behave like synchronous code. It's the modern, preferred way to work with promises.

โšก Sequential vs Parallel Execution

function slowTask(name, ms) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`${name} completed`);
      resolve(name);
    }, ms);
  });
}

// โŒ SLOW: Sequential (300ms total)
async function runSequential() {
  console.log('=== Sequential ===');
  const task1 = await slowTask('Task 1', 100); // Wait 100ms
  const task2 = await slowTask('Task 2', 100); // Then wait 100ms
  const task3 = await slowTask('Task 3', 100); // Then wait 100ms
  console.log('Sequential done'); // ~300ms total
}

// โœ… FAST: Parallel (100ms total)
async function runParallel() {
  console.log('=== Parallel ===');
  const [task1, task2, task3] = await Promise.all([
    slowTask('Task 1', 100),
    slowTask('Task 2', 100),
    slowTask('Task 3', 100)
  ]);
  console.log('Parallel done'); // ~100ms total
}

// Test: runParallel();
function fakeApi(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) resolve({ id, name: 'Item ' + id });
      else reject(new Error('invalid id'));
    }, 60);
  });
}

// using then/catch
fakeApi(1)
  .then(x => fakeApi(x.id + 1))
  .then(x => console.log('then chain got:', x))
  .catch(e => console.error('then chain error:', e.message));

// using async/await + try/catch
async function run() {
  try {
    const a = await fakeApi(1);
    const b = await fakeApi(a.id + 1);
    console.log('await got:', b);
  } catch (e) {
    console.error('await error:', e.message);
  }
}
run();

// Real-life with async/await: users โ†’ details
function fetchUsersP(){
  return new Promise((resolve) => setTimeout(() => resolve([
    {id:1,name:'A'}, {id:2,name:'B'}
  ]), 30));
}
function fetchDetailsP(id){
  return new Promise((resolve) => setTimeout(() => resolve({ userId:id, score:id*10 }), 30));
}

async function runUsers(){
  try{
    const users = await fetchUsersP();
    const detailed = await Promise.all(users.map(async (u) => ({
      id: u.id,
      name: u.name,
      details: await fetchDetailsP(u.id)
    })));
    console.log('all done (await):', detailed);
  }catch(e){
    console.error('await users error:', e.message);
  }
}
runUsers();
Async vs sync: Synchronous code runs topโ€‘toโ€‘bottom and blocks on each step. Asynchronous code schedules work (timers, network, I/O) and continues; results arrive later via callbacks or promises. async/await makes promise code read like sync, but itโ€™s still nonโ€‘blocking.

Keyboard Shortcuts

Navigation

J Next section
K Previous section
T Toggle sidebar
Esc Close sidebar

Other

L Cycle language
? Show this help