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; }
โ HoistedOwn thisarguments
๐Expression
const add = function(a, b) { return a + b; };
โ Not hoistedOwn thisarguments
โก๏ธArrow Function
const add = (a, b) => { return a + b; };
โ Not hoistedLexical 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 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 lexicalthis (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 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/setdelete 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 = objfunc() โ this = undefined/window
โก๏ธArrow Function
๐ Lexical this
Inherits from where defined
Always uses outer thisPerfect 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 outerthis. Classic function has a dynamic this
depending on how itโs called.
// 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' });
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.
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
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
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.
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:
Function boundary: Catch at the top level of your function if it's public/exported
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));
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.
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.