Language Refresher

Variables & Types

πŸ“¦ Variable Declarations

βœ…
const
Block-scoped
Cannot reassign
πŸ‘
let
Block-scoped
Can reassign
USE WHEN NEEDED
⚠️
var
Leaks outside blocks
Hoisted*
AVOID
πŸ“š *What is Hoisting?
JavaScript moves var and function declarations to the top of their scope before execution. This means you can use them before they appear in code. With var, the value is undefined until assigned. const/let throw an error if used before declaration.

🏷️ JavaScript Types (typeof results)

number string boolean undefined object function bigint symbol
⚠️ Quirk: typeof null === "object" (historical bug)
πŸ“¦
Variables
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

πŸ”€ Logical Operators at a Glance

&&
AND
Both must be true
||
OR
At least one true
!
NOT
Inverts value
? :
Ternary
Inline if/else
⚠️ Equality: == vs ===
== Loose (type coercion)
"5" == 5 β†’ true
=== Strict (prefer!)
"5" === 5 β†’ false
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)

// == vs === : Loose vs Strict Equality
// == compares values only (with type coercion)
// === compares BOTH value AND type (no coercion - prefer this!)
console.log(2 == "2");   // true  (loose: "2" converted to 2)
console.log(2 === "2");  // false (strict: number β‰  string)
console.log(5 == "5");   // true  (loose - avoid!)
console.log(5 === "5");  // false (strict - prefer!)

Loops

πŸ”„ Loop Types at a Glance

πŸ”’ for
Classic counter loop
for (i=0; i<n; i++)
πŸ“¦ for...of BEST FOR ARRAYS
Iterate values
for (v of array)
πŸ”‘ for...in FOR OBJECTS
Iterate keys
for (key in obj)
⏳ while
Check first, then run
while (cond) { }
πŸ” do...while
Run first, then check
do { } while (cond)
πŸ’‘ for...of for arrays (values)  β€’  for...in for objects (keys)
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 - can be called before it appears)
function add(a, b) {
  return a + b;
}

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

// Arrow function (modern, concise)
const addArrow = (a, b) => 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.

Function Names (for debugging)

// Function names appear in stack traces when debugging
function hello() {}
const greet = function () {};
const wave = () => {};

console.log(hello.name); // "hello"
console.log(greet.name); // "greet" (modern JS infers from variable)
console.log(wave.name);  // "wave" (arrow functions too!)
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

⚑ Arrow Function Syntax at a Glance

(parameters) => expression | { statements }
Zero Parameters
() => Date.now()
One Parameter
x => x * 2
Multiple Parameters
(a, b) => a + b
Block Body
(x) => { return x; }
βœ“ Shorter syntax
βœ“ Lexical this
βœ— No own arguments
βœ— Cannot be constructors
πŸ’‘ Return Rules
Expression body (no braces)
x => x * 2 β†’ implicit return
Block body (with braces)
x => { return x * 2; }
Return object literal
(x,y) => ({ x, y })

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

➑️
Arrow Syntax
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:
πŸ“
String Handling
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:
πŸ“¦
Use Cases
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

JSON: Data Interchange Format

The Problem: Storing Data in Text Format

Before standardized formats like JSON, storing structured data in text files was a programmer's nightmare. Every developer had their own encoding rules, leading to chaos and brittle code.

⚠️ The Pre-Standard Era: Custom Encoding Chaos
Scenario: Store a list of users with name, age, and email in a text file
πŸ‘¨β€πŸ’» Programmer A's Approach: Comma-separated
Alice,25,alice@example.com
Bob,30,bob@example.com
Charlie,28,charlie@company.org
❌ Problems:
  • What if name contains a comma? "Smith, John" breaks parsing
  • Adding a new field (city) requires updating ALL encoding/decoding logic
  • No way to represent optional fields (missing email?)
πŸ‘¨β€πŸ’» Programmer B's Approach: Fixed-width columns
Alice     25alice@example.com    
Bob       30bob@example.com      
Charlie   28charlie@company.org  
❌ Problems:
  • Names longer than 10 chars get truncated or break the format
  • Wastes space with padding for short values
  • Changing column widths = rewrite entire file
πŸ‘¨β€πŸ’» Programmer C's Approach: Custom delimiters
name:Alice|age:25|email:alice@example.com
name:Bob|age:30|email:bob@example.com
name:Charlie|age:28|email:charlie@company.org
❌ Problems:
  • What if data contains | or : characters?
  • Complex parsing logic for nested data structures
  • Every app needs custom parser implementation
πŸ’‘ The Core Issue:

Without conventions, encoding and decoding becomes increasingly complex. Adding fields, handling special characters, supporting nested data, and ensuring compatibility across systems requires constant rewrites of parsing code.

The Solution: Standardized Formats

Programmers realized they needed a convention β€” a universal format that everyone agrees on. This led to the creation of standardized data interchange formats.

πŸ“„ First Came XML (1998)

XML (eXtensible Markup Language) was the first widely-adopted standard for data interchange.

<?xml version="1.0" encoding="UTF-8"?>
<users>
  <user>
    <name>Alice</name>
    <age>25</age>
    <email>alice@example.com</email>
  </user>
  <user>
    <name>Bob</name>
    <age>30</age>
    <email>bob@example.com</email>
  </user>
</users>
βœ… Advantages:
  • Universal standard with parsers in every language
  • Self-describing with clear structure
  • Supports attributes and namespaces
  • Great for document-oriented data
❌ Disadvantages:
  • Verbose β€” lots of redundant closing tags
  • Larger file sizes (bandwidth waste)
  • Complex parsing (DOM/SAX parsers)
  • Doesn't map naturally to programming objects
πŸš€ Then Came JSON (2001)

JSON (JavaScript Object Notation) was created by Douglas Crockford as a lightweight alternative to XML. It's based on JavaScript object syntax but is language-independent.

{
  "users": [
    {
      "name": "Alice",
      "age": 25,
      "email": "alice@example.com"
    },
    {
      "name": "Bob",
      "age": 30,
      "email": "bob@example.com"
    }
  ]
}
🎯 Why JSON Became Dominant:
1️⃣
Lightweight & Compact

Less verbose than XML. The same data is 30-50% smaller, saving bandwidth and improving performance.

2️⃣
Native JavaScript Support

JSON maps directly to JavaScript objects and arrays. Parsing is trivial with JSON.parse() and JSON.stringify().

3️⃣
Simple & Readable

Easy for humans to read and write. Easy for machines to parse and generate.

4️⃣
Language Independent

Despite its JavaScript origins, JSON has parsers in every major programming language (Python, Java, C#, Go, Rust, etc.).

5️⃣
Perfect for APIs

Became the de facto standard for REST APIs. Easier to work with than XML in modern web applications.

JSON Syntax & Data Types

JSON supports a limited but practical set of data types:

πŸ“ String
"Hello World"
Double quotes only
πŸ”’ Number
42, 3.14, -10, 1e5
Integer or float
βœ… Boolean
true, false
Lowercase only
β­• Null
null
Represents no value
πŸ“¦ Array
[1, 2, 3]
Ordered list
πŸ—‚οΈ Object
{"key": "value"}
Key-value pairs
⚠️ JSON vs JavaScript Object Literals:
Key Differences (Click to expand)
  • Keys must be double-quoted strings in JSON: {"name": "Alice"} not {name: "Alice"}
  • Strings must use double quotes: "hello" not 'hello'
  • No trailing commas: [1,2,3] not [1,2,3,]
  • No comments allowed in JSON (pure data format)
  • No functions, undefined, or Date objects β€” only the 6 basic types
  • No special values: NaN, Infinity, or undefined are not valid JSON

Working with JSON in JavaScript

JSON.parse() β€” Convert JSON string to JavaScript object
// JSON string (from API, file, etc.)
const jsonString = '{"name":"Alice","age":25,"skills":["JavaScript","Python","SQL"]}';

// Parse JSON string β†’ JavaScript object
const user = JSON.parse(jsonString);

console.log(user.name);        // "Alice"
console.log(user.skills[0]);   // "JavaScript"
console.log(typeof user);      // "object"

// Parse array
const numbersJSON = '[1, 2, 3, 4, 5]';
const numbers = JSON.parse(numbersJSON);
console.log(numbers);          // [1, 2, 3, 4, 5]
console.log(Array.isArray(numbers)); // true
JSON.stringify() β€” Convert JavaScript object to JSON string
const user = {
  name: "Bob",
  age: 30,
  email: "bob@example.com",
  isActive: true,
  skills: ["React", "Node.js"]
};

// Convert to JSON string
const jsonString = JSON.stringify(user);
console.log(jsonString);
// {"name":"Bob","age":30,"email":"bob@example.com","isActive":true,"skills":["React","Node.js"]}

// Pretty-print with indentation (great for debugging)
const prettyJSON = JSON.stringify(user, null, 2);
console.log(prettyJSON);
/*
{
  "name": "Bob",
  "age": 30,
  "email": "bob@example.com",
  "isActive": true,
  "skills": [
    "React",
    "Node.js"
  ]
}
*/
Filtering properties with replacer function
const user = {
  name: "Alice",
  age: 25,
  password: "secret123",  // We don't want to include this!
  email: "alice@example.com"
};

// Option 1: Array of allowed keys
const safeJSON1 = JSON.stringify(user, ["name", "email"]);
console.log(safeJSON1); // {"name":"Alice","email":"alice@example.com"}

// Option 2: Replacer function for more control
const safeJSON2 = JSON.stringify(user, (key, value) => {
  // Skip password field
  if (key === "password") return undefined;
  return value;
});
console.log(safeJSON2); // {"name":"Alice","age":25,"email":"alice@example.com"}
Handling JSON parse errors
// Invalid JSON examples
const invalidJSON = [
  '{"name": "Alice"',           // Missing closing brace
  "{name: 'Alice'}",            // Single quotes & unquoted key
  '{"age": undefined}',         // undefined not allowed
  '{"values": [1, 2, 3,]}'      // Trailing comma
];

invalidJSON.forEach(jsonStr => {
  try {
    const result = JSON.parse(jsonStr);
    console.log("Parsed:", result);
  } catch (error) {
    console.error("Parse error:", error.message);
  }
});

// Safe parsing with default fallback
function safeParse(jsonString, defaultValue = null) {
  try {
    return JSON.parse(jsonString);
  } catch {
    return defaultValue;
  }
}

const data = safeParse('invalid json', { name: "Default" });
console.log(data); // { name: "Default" }
🌐 Real-World JSON Use Cases
πŸ“‘
REST APIs

JSON is the standard format for sending/receiving data from web APIs. Fetch data from servers, send form data, handle responses.

βš™οΈ
Configuration Files

package.json, tsconfig.json, VS Code settings β€” JSON is everywhere in modern development tooling.

πŸ’Ύ
Local Storage

Store JavaScript objects in browser localStorage by converting them to JSON strings.

πŸ—„οΈ
NoSQL Databases

MongoDB and other NoSQL databases store data in JSON-like format (BSON = Binary JSON).

πŸ“¦
Data Exchange

Transfer data between different systems, languages, and platforms in a universal format.

πŸ’‘ Key Takeaway: JSON solved the chaos of custom encoding formats by providing a simple, universal standard that's easy to read, write, and parse. It's lightweight, language-independent, and has become the backbone of modern web communication.

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?
πŸ”§ Complete Array Methods Reference
πŸ’‘ Hover over any method to see details and examples
πŸ” Iteration Methods
.forEach() .map() .filter() .reduce() .reduceRight() .flatMap()
⚠️ Modification Methods (Mutate Original)
.push() .pop() .shift() .unshift() .splice() .sort() .reverse() .fill() .copyWithin()
✨ Non-Mutating Methods (Return New Array)
.slice() .concat() .flat() .toReversed() .toSorted() .toSpliced() .with()
πŸ”„ Conversion & Join Methods
.join() .toString() .toLocaleString()
🏭 Static Methods (Array.method)
Array.isArray() Array.from() Array.of()
πŸ”— Iterator Methods
.keys() .values() .entries() [Symbol.iterator]()
⚠️
.sort() β€” mutates original array! Use [...arr].sort() or .toSorted() 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

πŸ• The Real-World Problem: Waiting in JavaScript

Imagine you're ordering pizza online. You don't sit there staring at the screen doing nothing while waiting for delivery. You go watch TV, do homework, play games β€” you multitask. When the pizza arrives, the doorbell rings, and you handle it.

JavaScript works the same way! Some operations take time (downloading files, fetching data from servers, reading files). If JavaScript just stopped and waited for each task, your webpage would freeze β€” buttons wouldn't work, animations would stop, users would leave frustrated.

❌ Blocking (Bad)
1. Order pizza
2. Wait... doing nothing...
3. Still waiting... page frozen...
4. Pizza arrives
5. Finally continue!
βœ… Non-Blocking (Good)
1. Order pizza
2. Watch TV (page responsive!)
3. Do homework (scrolling works!)
4. πŸ”” Doorbell! Pizza arrives
5. Handle delivery, then continue
⏱️

Timeline: Synchronous vs Asynchronous

Understanding the difference between sequential and parallel execution is crucial for writing efficient JavaScript code.

🚫
Synchronous (Blocking) β€” Tasks run one at a time

Each task must wait for the previous one to complete. The main thread is blocked β€” nothing else can happen.

Task 1
β†’
⏳ Wait...
β†’
Task 2
β†’
⏳ Wait...
β†’
Task 3
⏱️ Total Time Calculation:
Task1(100ms) + Task2(100ms) + Task3(100ms) =
300ms SLOW! ❌
⚑
Asynchronous (Non-blocking) β€” Tasks run in parallel

All tasks start immediately and run concurrently. The main thread stays responsive β€” other code can execute!

Start Task 1
↗️
running in background...
Start Task 2
↗️
running in background...
Start Task 3
↗️
running in background...
After 100ms, all tasks complete simultaneously:
βœ… Task 1
βœ… Task 2
βœ… Task 3
⚑ Total Time Calculation:
Since all tasks run in parallel, the total time is the longest task, not the sum:
max(100ms, 100ms, 100ms) =
100ms FAST! βœ…
πŸ’‘ Key Insight: By running tasks concurrently instead of sequentially, we achieve 3x better performance. This is why async programming is essential for modern web applications.

🎯 JavaScript Asynchronous: From Callbacks to Async/Await

  1. Callbacks (the original way) β€” Functions passed as arguments, but leads to "pyramid of doom"
  2. Promises (ES2015) β€” Objects representing future values, chainable with .then()
  3. Async/Await (ES2017) β€” Syntactic sugar over promises, looks like synchronous code

We'll explore each one, see their problems, and understand why we moved to the next solution.

1️⃣ Callbacks: The Original Async Pattern

What is a callback? A function you pass as an argument to another function, to be called later when something finishes.

πŸ’‘ Real-world analogy: You give your phone number to a restaurant. When your table is ready, they call you back. You don't stand there waiting β€” you do other things until they notify you!

Simple callback example: setTimeout

// The simplest async function: setTimeout
console.log('1. Ordering pizza...');

setTimeout(function() {
  console.log('3. πŸ• Pizza delivered!');
}, 2000); // Wait 2 seconds (2000ms)

console.log('2. Doing other stuff while waiting...');

// Output order:
// 1. Ordering pizza...
// 2. Doing other stuff while waiting...
// (2 second pause)
// 3. πŸ• Pizza delivered!
πŸ’‘ Key Insight: Notice the output order! Line 2 runs before line 3, even though line 3 is written first. That's because setTimeout schedules the callback for later β€” JavaScript doesn't wait, it continues executing the next line immediately.

The Problem: Callback Hell (the pyramid of doom)

When you need multiple async operations that depend on each other, callbacks get nested deeper and deeper. This creates a "pyramid" shape that's hard to read, maintain, and debug.

Dependency chain: In the 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 });
        });
      });
    });
  });
Problems with callbacks:
  • Deep nesting makes code hard to read ("arrow anti-pattern")
  • Error handling repeated in every callback (if (err) everywhere)
  • Hard to reason about the flow
  • Difficult to debug when things go wrong

2️⃣ Promises: A Better Way

What is a Promise? An object representing a value that will be available in the future. Think of it as a "receipt" for async work β€” it's either pending, fulfilled (success), or rejected (error).

πŸ’‘ 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!

✨ Why Use Promises? Key Advantages

πŸ”—
Escape Callback Hell
Promises flatten deeply nested callbacks into a readable chain. Instead of pyramid-shaped code, you get .then().then().then() β€” linear and easy to follow.
🎯
Async or Sync-Style
The same Promise can be consumed with .then() (async callbacks) or await (sync-looking code). You decide based on your use case β€” the API doesn't force a pattern.
⚑
Never Blocks the Thread
Even when using await (which looks synchronous), JavaScript never blocks. The event loop continues running β€” UI stays responsive, other code executes. It's just syntactic sugar over .then().
πŸ›‘οΈ
Centralized Error Handling
One .catch() at the end handles errors from any step in the chain. No more repetitive if (err) checks in every callback.

🎁 What is a Promise?

A Promise is an object that represents a future value. Think of it like a receipt when you order food:

πŸ• Real-world analogy: You order a pizza and get a receipt (the promise). The receipt isn't the pizza itself β€” it's a promise that you'll get pizza later. The receipt has two possible outcomes:
  • βœ… Fulfilled: Pizza arrives! (success)
  • ❌ Rejected: Restaurant is closed. (error)

πŸ“ Promise Syntax - Step by Step

Step 1: Creating a Promise

// Creating a promise that will succeed after 1 second
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Success! πŸŽ‰');  // Call resolve() when successful
  }, 1000);
});

console.log('Promise created:', myPromise);  // Shows: Promise {  }

Key concepts:

  • new Promise() β€” Creates a new promise object
  • resolve β€” Function to call when the task succeeds
  • reject β€” Function to call when the task fails
  • The promise starts in pending state

Step 2: Using the Promise with .then()

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Data loaded!'), 1000);
});

// .then() runs when the promise resolves
myPromise.then((result) => {
  console.log('Got result:', result);  // Runs after 1 second
});

console.log('Promise started, waiting...');

Step 3: Handling Errors with .catch()

const failingPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('Something went wrong! ❌'));  // Call reject() on error
  }, 1000);
});

failingPromise
  .then((result) => {
    console.log('Success:', result);  // Won't run
  })
  .catch((error) => {
    console.error('Caught error:', error.message);  // Runs after 1 second
  });

From callbacks to promises

Now that you understand the basics, let's see how promises make our code cleaner:

// 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

One of the best features of promises: one .catch() at the end handles errors from ANY step in the chain!

// Example 1: Error in step 2 is caught by the single .catch() at the end
function getUser() {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id: 1, name: 'Alice' }), 100);
  });
}

function getProfile(user) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Simulate error in step 2
      reject(new Error('Profile not found! ❌'));
    }, 100);
  });
}

function getSettings(profile) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ theme: 'dark' }), 100);
  });
}

// Chain multiple steps - error in ANY step goes to the single .catch()
getUser()
  .then(user => {
    console.log('βœ… Step 1: Got user:', user.name);
    return getProfile(user);  // This will fail
  })
  .then(profile => {
    console.log('βœ… Step 2: Got profile:', profile);  // Won't run
    return getSettings(profile);
  })
  .then(settings => {
    console.log('βœ… Step 3: Got settings:', settings);  // Won't run
  })
  .catch(err => {
    // This ONE catch handles errors from ANY step above!
    console.error('🚨 Caught error from chain:', err.message);
  });

console.log('Chain started, waiting...');
πŸ”‘ Key Point: Errors "bubble up" through the chain. If step 2 fails, steps 3, 4, 5... all get skipped and the error goes straight to .catch(). This is much cleaner than callbacks where you need error handling in every step!

πŸ“Š Interactive Promise Lifecycle Diagram

Use the controls below to see how promises move through different states. This interactive diagram shows the full promise lifecycle:

Step 0 / 0

Pending ⏳ Waiting not settled yet 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). Use the controls above to explore different scenarios.

πŸ“ Code Examples

Click on a tab to see the code for each promise pattern:

Resolve: Basic Success Flow

When a promise resolves successfully, the value flows through .then() handlers.

// Promise resolves β†’ .then() receives 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(); // Transform and pass to next .then()
})
.then(transformed => {
  console.log(transformed); // "SUCCESS! DATA LOADED"
});
Reject: Error Handling

When a promise rejects, .catch() catches the error.

// 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'); // Skipped!
})
.catch(error => {
  console.error('Caught:', error.message); // "Caught: Network timeout"
});
Recovery: Continuing After Error

.catch() can recover from errors by returning a value, transforming failure into success. Key difference from .finally(): .catch() with a return value changes the outcome and continues the chain, while .finally() only does cleanup without changing anything.

// .catch() recovers: transforms error β†’ success
const promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('API failed')), 1000);
});

promise
  .then(value => {
    console.log('Step 1: This will NOT run'); // Skipped!
    return value;
  })
  .catch(error => {
    console.error('Step 2: Error caught:', error.message);
    return 'Using cached data'; // Recovery: error β†’ success!
  })
  .then(data => {
    console.log('Step 3: Chain continues:', data); // This WILL run!
  });
Chain Error: .then() Throws

If a .then() handler throws an error, it jumps to the next .catch().

// Error in .then() jumps to .catch()
const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve({ id: 42, name: 'Alice' }), 500);
});

promise.then(user => {
  console.log('User received:', user); // Logs user data
  
  if (user.id < 100) {
    throw new Error('User ID too low'); // Throws error!
  }
  return user;
})
.catch(error => {
  console.error('Error caught:', error.message); // "User ID too low"
});
Chain Success: Returning Promises

When .then() returns a promise, the chain waits for it to resolve before continuing.

// Chaining: .then() returns a promise β†’ waits for it
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); // First promise resolves
  return fetchUserPosts(user.id); // Returns new promise!
})
.then(posts => {
  console.log('Got posts:', posts); // Second promise resolves
});
Finally: Cleanup Handler

.finally() always runs, whether the promise resolves or rejects. Perfect for cleanup.

// .finally() always runs β†’ perfect for cleanup
let isLoading = true;
console.log('Loading:', isLoading); // true

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

fetchData()
  .then(data => console.log('Success:', data))
  .catch(error => console.error('Error:', error.message))
  .finally(() => {
    isLoading = false; // Cleanup happens here
    console.log('Loading:', isLoading); // false (always runs!)
  });

⚑ Promise Static Methods

Shorthand methods for creating promises instantly - you'll see these everywhere in modern code!

βœ… Promise.resolve(value)

Creates a promise that's already fulfilled with the given value. The promise is resolved immediately (in the same tick).

// These two are equivalent:
const p1 = new Promise((resolve) => resolve('Hello'));
const p2 = Promise.resolve('Hello'); // Shorthand ✨

p2.then(value => console.log(value)); // "Hello"

// Real use case: Ensuring a function always returns a Promise
function getConfig(useCache) {
  if (useCache) {
    // Synchronous: return cached value as a Promise
    return Promise.resolve({ theme: 'dark', lang: 'en' });
  }
  // Asynchronous: fetch from server
  return fetch('/api/config').then(res => res.json());
}

// Now both code paths return promises! 🎯
getConfig(true).then(config => console.log(config));

// ⚠️ Important: If you pass a Promise to Promise.resolve(), 
// it returns that same promise unchanged
const existingPromise = fetch('/data');
const samePromise = Promise.resolve(existingPromise);
console.log(existingPromise === samePromise); // true
❌ Promise.reject(reason)

Creates a promise that's already rejected with the given reason (usually an Error). The promise is rejected immediately (in the same tick).

// These two are equivalent:
const p1 = new Promise((resolve, reject) => {
  reject(new Error('Failed'));
});
const p2 = Promise.reject(new Error('Failed')); // Shorthand ✨

p2.catch(error => console.error('Error:', error.message));

// Real use case: Early validation/exit
function getUserById(id) {
  // Validate input first
  if (!id || typeof id !== 'number') {
    return Promise.reject(new Error('Invalid user ID'));
  }
  
  if (id < 0) {
    return Promise.reject(new Error('User ID must be positive'));
  }
  
  // Only make API call if validation passes
  return fetch(`/api/users/${id}`).then(res => res.json());
}

// Test with invalid input
getUserById(-5)
  .then(user => console.log('User:', user))
  .catch(error => console.error('❌', error.message)); // "User ID must be positive"

// Test with valid input
getUserById(123)
  .then(user => console.log('βœ… User:', user))
  .catch(error => console.error('❌', error.message));
πŸ’‘
Key Points:
  • Immediate settlement: Both methods create promises that are already settled (no async work happens)
  • Microtask queue: Even though settled immediately, .then()/.catch() callbacks still run asynchronously (in the next microtask)
  • Common uses:
    • Promise.resolve(): Converting sync values to promises, ensuring consistent return types, testing
    • Promise.reject(): Early validation errors, input checking, controlled failures

3️⃣ Async/Await: The Modern Way

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.

Basic Usage Example

Let's start with a practical example to see how async/await works:

// Regular promise-based function
function fetchUserData(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: userId, name: 'Alice', email: 'alice@example.com' });
    }, 1000);
  });
}

// Using async/await (modern way)
async function getUserInfo(userId) {
  console.log('Fetching user...');
  const user = await fetchUserData(userId); // Wait for promise to resolve
  console.log('User:', user);
  return user; // Automatically wrapped in a Promise!
}

getUserInfo(123);
Understanding the async Keyword
// Rule 1: async function ALWAYS returns a Promise
async function example1() {
  return 'Hello'; // Automatically wrapped in Promise.resolve('Hello')
}

example1().then(result => console.log(result)); // "Hello"

// Rule 2: Even explicit values become promises
async function example2() {
  return Promise.resolve(42);
}

example2().then(result => console.log(result)); // 42

// Rule 3: async without await is unnecessary (but valid)
async function example3() {
  console.log('No await here'); // Just a normal function that returns Promise
  return 'Done';
}

example3();
Understanding the await Keyword
// Rule 1: await can ONLY be used inside async functions
async function mustBeAsync() {
  const result = await Promise.resolve('Valid');
  console.log(result); // "Valid"
}

// ❌ This would be an ERROR (uncomment to see):
// function notAsync() {
//   const result = await Promise.resolve('Invalid'); // SyntaxError!
// }

// Rule 2: await pauses the function until Promise resolves
async function sequential() {
  console.log('Start');
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('After 1 second'); // Waits here
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('After 2 seconds'); // Waits here too
}

sequential();

// Rule 3: You can have async without await (but not await without async)
async function noAwait() {
  return 'No await needed here!'; // Still valid
}

noAwait().then(console.log);
Error Handling with try/catch
function fetchData(shouldFail) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error('API failed!'));
      } else {
        resolve({ data: 'Success!' });
      }
    }, 500);
  });
}

// Modern error handling with try/catch
async function handleData(shouldFail) {
  try {
    const result = await fetchData(shouldFail);
    console.log('Got data:', result);
    return result;
  } catch (error) {
    console.error('Error caught:', error.message);
    return { data: 'Fallback data' }; // Recovery!
  }
}

// Test both success and failure
handleData(false); // Success
handleData(true);  // Error caught and handled
πŸ’‘ Key Takeaways:
  • async function always returns a Promise
  • await can only be used inside async functions
  • async without await is valid but usually unnecessary
  • try/catch for error handling (cleaner than .catch())
  • Locally blocking, globally non-blocking! await pauses only its async function, but other code/events continue running

⚑ Promise Combinators: Running Multiple Promises

When working with multiple promises, JavaScript provides powerful methods to control how they execute. All these methods take an array of promises as parameter and return a new promise.

πŸ“‹ What they accept and return:
  • Parameter: An iterable (usually an array) of promises: [promise1, promise2, ...]
  • Returns: A new promise that resolves with:
    • Promise.all() β†’ Array of all resolved values (same order)
    • Promise.allSettled() β†’ Array of result objects with {status, value/reason}
    • Promise.race() β†’ The value/reason of the first settled promise
    • Promise.any() β†’ The value of the first fulfilled promise
Sequential vs Parallel Execution

Understanding the difference between await (sequential) and Promise.all() (parallel) is critical for performance:

function fetchUser(id) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`User ${id} fetched`);
      resolve({ id, name: `User${id}` });
    }, 1000);
  });
}

// ❌ SLOW: Sequential with await (3 seconds total)
async function sequentialFetch() {
  console.log('=== Sequential (SLOW) ===');
  const start = Date.now();
  
  const user1 = await fetchUser(1); // Wait 1s
  const user2 = await fetchUser(2); // Wait 1s  
  const user3 = await fetchUser(3); // Wait 1s
  
  console.log('Total time:', Date.now() - start, 'ms');
  return [user1, user2, user3];
}

// βœ… FAST: Parallel with Promise.all (1 second total)
async function parallelFetch() {
  console.log('=== Parallel (FAST) ===');
  const start = Date.now();
  
  // Start all promises at once!
  const users = await Promise.all([
    fetchUser(1),
    fetchUser(2),
    fetchUser(3)
  ]);
  
  console.log('Total time:', Date.now() - start, 'ms');
  return users;
}

// Try both to see the difference
sequentialFetch().then(() => parallelFetch());
⚠️ Performance Trap: Using await in a loop or sequentially for independent operations is slow! If tasks don't depend on each other, use Promise.all() to run them in parallel.
Promise.all(promiseArray) - Wait for All

Takes: An array of promises
Returns: A promise that resolves with an array of all resolved values (in the same order)
Behavior: Runs all promises in parallel. If any promise rejects, the entire operation fails immediately.

function apiCall(id, delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 3) {
        reject(new Error(`API ${id} failed!`));
      } else {
        resolve({ id, data: `Result ${id}` });
      }
    }, delay);
  });
}

// Promise.all - all must succeed
async function usePromiseAll() {
  try {
    const results = await Promise.all([
      apiCall(1, 500),
      apiCall(2, 1000),
      // apiCall(3, 200)  // Uncomment to see it fail
    ]);
    console.log('All succeeded:', results);
  } catch (error) {
    console.error('One failed, all rejected:', error.message);
  }
}

usePromiseAll();
Promise.allSettled(promiseArray) - Wait for All, Get All Results

Takes: An array of promises
Returns: A promise that resolves with an array of result objects: [{status: 'fulfilled', value: ...}, {status: 'rejected', reason: ...}]
Behavior: Runs all promises in parallel. Never rejects - waits for all to finish regardless of success/failure.

// Promise.allSettled - get all results (success or failure)
async function usePromiseAllSettled() {
  const results = await Promise.allSettled([
    Promise.resolve('Success 1'),
    Promise.reject(new Error('Failed 2')),
    Promise.resolve('Success 3')
  ]);
  
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`${index}: βœ…`, result.value);
    } else {
      console.log(`${index}: ❌`, result.reason.message);
    }
  });
}

usePromiseAllSettled();
Promise.race(promiseArray) - First One Wins

Takes: An array of promises
Returns: A promise that resolves/rejects with the value/reason of the first settled promise
Behavior: Returns as soon as ANY promise settles (resolve OR reject). Others are ignored.

function slowAPI() {
  return new Promise(resolve => {
    setTimeout(() => resolve('Slow API (2s)'), 2000);
  });
}

function fastAPI() {
  return new Promise(resolve => {
    setTimeout(() => resolve('Fast API (500ms)'), 500);
  });
}

// Promise.race - first to finish wins
async function usePromiseRace() {
  const winner = await Promise.race([
    slowAPI(),
    fastAPI()
  ]);
  console.log('Winner:', winner); // "Fast API (500ms)"
}

// Common use case: Timeout
async function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Timeout!')), ms);
  });
  
  return Promise.race([promise, timeout]);
}

// Test timeout
withTimeout(slowAPI(), 1000)
  .then(result => console.log('Success:', result))
  .catch(error => console.error('Error:', error.message)); // "Timeout!"

usePromiseRace();
Promise.any(promiseArray) - First Success Wins

Takes: An array of promises
Returns: A promise that resolves with the value of the first fulfilled promise
Behavior: Returns as soon as ANY promise fulfills. Ignores rejections unless ALL promises reject (then throws AggregateError).

// Promise.any - first success wins, ignores failures
async function usePromiseAny() {
  try {
    const result = await Promise.any([
      Promise.reject(new Error('Failed 1')),
      Promise.resolve('Success 2'),
      Promise.reject(new Error('Failed 3'))
    ]);
    console.log('First success:', result); // "Success 2"
  } catch (error) {
    console.error('All failed:', error);
  }
}

usePromiseAny();
πŸ“Š Quick Comparison:
  • Promise.all() - Waits for all, fails if any fails ⚑ Best for: All tasks must succeed
  • Promise.allSettled() - Waits for all, never fails πŸ“Š Best for: Need all results regardless
  • Promise.race() - First to finish (success or failure) 🏁 Best for: Timeouts, fastest response
  • Promise.any() - First success wins 🎯 Best for: Fallback APIs, redundancy

πŸ”„ Async/Await vs Promises Comparison

Both approaches work with promises, but async/await provides cleaner, more readable code:

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) resolve({ id, name: `User ${id}` });
      else reject(new Error('Invalid ID'));
    }, 500);
  });
}

// ❢ OLD WAY: Using .then()/.catch() chains
console.log('=== Promise Chains (.then) ===');
fetchUser(1)
  .then(user => {
    console.log('Got user:', user);
    return fetchUser(user.id + 1);
  })
  .then(user2 => {
    console.log('Got user 2:', user2);
  })
  .catch(error => {
    console.error('Error:', error.message);
  });

// ❷ MODERN WAY: Using async/await + try/catch
console.log('=== Async/Await ===');
async function getUserChain() {
  try {
    const user = await fetchUser(1);
    console.log('Got user:', user);
    
    const user2 = await fetchUser(user.id + 1);
    console.log('Got user 2:', user2);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

getUserChain();

πŸ“Š Quick Comparison: Callbacks vs Promises vs Async/Await

πŸ›‘οΈ
Error Handling
Callbacks: Manual if (err) in every callback
Promises: Single .catch() for entire chain
Async/Await: try/catch block (most readable)
πŸ“–
Readability
Callbacks: ❌ Pyramid of doom, hard to follow
Promises: βœ… Flat .then() chains
Async/Await: βœ…βœ… Looks like synchronous code
πŸ”—
Sequential Operations
Callbacks: Deep nesting required
Promises: .then().then().then()
Async/Await: const a = await x; const b = await y;
⚑
Parallel Operations
Callbacks: Complex coordination needed
Promises: Promise.all([p1, p2, p3])
Async/Await: await Promise.all([p1, p2, p3])
🌐
Browser Support
Callbacks: βœ… All browsers (original JS)
Promises: βœ… ES2015+ (IE11 needs polyfill)
Async/Await: βœ… ES2017+ (modern browsers)

⚠️ Common Mistakes to Avoid

  1. Forgetting return in .then():
    // ❌ BAD - forgot to return promise
    promise.then(x => { fetchNext(x); })  // Promise lost!
      .then(y => console.log(y));         // y is undefined
    
    // βœ… GOOD - returns promise so chain waits
    promise.then(x => fetchNext(x))      // Implicit return
      .then(y => console.log(y));
  2. Sequential await when parallel is possible:
    // ❌ SLOW - waits 200ms + 200ms = 400ms
    const a = await fetchA(); // 200ms
    const b = await fetchB(); // 200ms
    
    // βœ… FAST - waits max(200ms, 200ms) = 200ms
    const [a, b] = await Promise.all([fetchA(), fetchB()]);
  3. Not handling errors:
    // ❌ BAD - unhandled rejection!
    async function bad() {
      const data = await fetchData(); // If this fails, app crashes
    }
    
    // βœ… GOOD
    async function good() {
      try {
        const data = await fetchData();
      } catch (error) {
        console.error('Handled:', error);
      }
    }
  4. Using async without await: If you don't use await inside an async function, you don't need async!
  5. Forgetting await keyword:
    // ❌ BAD - missing await, user is a Promise object!
    const user = fetchUser(); // Promise { pending }
    
    // βœ… GOOD
    const user = await fetchUser(); // { id: 1, name: 'Alice' }

🎯 When to Use What?

✨
Use Async/Await for new code β€” most readable and maintainable
βš™οΈ
Use Promises when you need fine-grained control or mixing with older code
⚠️
Avoid Callbacks for new async code β€” use only when required by libraries
πŸ’‘
Remember:

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 β€” the event loop continues running, UI stays responsive.

🎯 Next Step

Ready to Bring JavaScript to Life?

Now that you've mastered modern JavaScript syntax, it's time to interact with web pages using the DOM, handle events, and make AJAX calls!

🎨 DOM Manipulation
⚑ Event Handling
🌐 Fetch & AJAX
πŸš€ Browser APIs
Continue to Browser JavaScript β†’
πŸ“

Ready to Practice?

Test your knowledge with practice exercises and prepare for your exam.

Go to Exercises β†’
? Raccourcis

Keyboard Shortcuts

Navigation

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

Other

L Cycle language
? Show this help