Functions, Callbacks, Objects, Arrays, Errors, Promises & ES6+ Features
Language Refresher
Variables & Types
π¦ Variable Declarations
β
const
Block-scoped
Cannot reassign
RECOMMENDED
π
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.
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...ofBEST FOR ARRAYS
Iterate values
for (v of array)
πfor...inFOR 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; }
β 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 - 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 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 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
β‘οΈ
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 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.
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
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.
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.
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
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
π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
Callbacks (the original way) β Functions passed as arguments, but leads to "pyramid of doom"
Promises (ES2015) β Objects representing future values, chainable with .then()
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));
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
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.
.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.
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)
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
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:
β οΈ 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).
// β 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);
}
}
Using async without await: If you don't use await inside an async function, you don't need async!
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!