Dark mode

JavaScript Notes

The programming language of the web

Introduction to JavaScript

JavaScript is a programming language that enables interactive web pages and is an essential part of web applications. It's what allows websites to respond to user interactions, update content dynamically, and create a more engaging user experience.

JavaScript Basics

What is JavaScript?

JavaScript is a high-level, interpreted programming language primarily used for creating interactive effects within web browsers. It's one of the core technologies of the World Wide Web, alongside HTML and CSS.

JavaScript allows you to:

  • Add interactivity to web pages
  • Create dynamic content updates
  • Control multimedia
  • Animate images
  • Build web and mobile applications

Despite its name, JavaScript is not related to Java. It was originally created in 10 days by Brendan Eich while he was working at Netscape in 1995.

Example
// Basic JavaScript example
console.log('Hello, World!');

// Using JavaScript to change an HTML element
document.getElementById('demo').innerHTML = 'Hello JavaScript!';

// Creating a simple function
function greet(name) {
  return 'Hello, ' + name + '!';
}

console.log(greet('Learner'));

Variables and Data Types

Variables are containers for storing data values. In JavaScript, you declare variables using var, let, or const:

  • var: Function-scoped, can be redeclared and updated (older way, less recommended now)
  • let: Block-scoped, can be updated but not redeclared within the same scope
  • const: Block-scoped, cannot be updated or redeclared (for constant values)

JavaScript has several data types:

  • Primitive types:
    • String: Text values ('Hello', "World")
    • Number: Numeric values (42, 3.14)
    • Boolean: true or false
    • undefined: Variable declared but not assigned a value
    • null: Intentional absence of value
    • Symbol: Unique and immutable value (ES6)
    • BigInt: For large integers (ES2020)
  • Reference types:
    • Object: Collections of key-value pairs
    • Array: Ordered lists
    • Function: Code blocks that can be executed
Example
// Variable declarations
var oldWay = 'Using var'; // Function-scoped
let modernWay = 'Using let'; // Block-scoped
const CONSTANT = 'Cannot change'; // Cannot be reassigned

// Primitive data types
let name = 'John'; // String
let age = 30; // Number
let isStudent = true; // Boolean
let job = undefined; // Undefined
let salary = null; // Null

// Reference data types
// Object
let person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30
};

// Array
let colors = ['red', 'green', 'blue'];

// Function
let greet = function() {
  return 'Hello!';
};

// Checking data types
console.log(typeof name); // 'string'
console.log(typeof age); // 'number'
console.log(typeof isStudent); // 'boolean'
console.log(typeof job); // 'undefined'
console.log(typeof salary); // 'object' (historical bug in JavaScript)
console.log(typeof person); // 'object'
console.log(typeof colors); // 'object' (arrays are objects in JavaScript)
console.log(typeof greet); // 'function'
Result
string
number
boolean
undefined
object
object
object
function

Operators

Operators are symbols that perform operations on operands (values and variables). JavaScript has several types of operators:

Arithmetic Operators

  • + Addition
  • - Subtraction
  • * Multiplication
  • / Division
  • % Modulus (remainder)
  • ** Exponentiation (ES2016)
  • ++ Increment
  • -- Decrement

Assignment Operators

  • =, +=, -=, *=, /=, %=, etc.

Comparison Operators

  • == Equal to (value only)
  • === Equal to (value and type)
  • != Not equal to (value only)
  • !== Not equal to (value and type)
  • > Greater than
  • < Less than
  • >= Greater than or equal to
  • <= Less than or equal to

Logical Operators

  • && Logical AND
  • || Logical OR
  • ! Logical NOT

Type Operators

  • typeof Returns the type of a variable
  • instanceof Returns true if an object is an instance of a specified type
Example
// Arithmetic operators
let a = 10;
let b = 5;
let sum = a + b;      // 15
let diff = a - b;     // 5
let product = a * b;  // 50
let quotient = a / b; // 2
let remainder = a % b; // 0
let power = a ** 2;    // 100

// Increment and decrement
let counter = 1;
counter++;  // counter is now 2
counter--;  // counter is now 1

// Assignment operators
let x = 10;
x += 5;  // Same as x = x + 5, x is now 15
x -= 3;  // Same as x = x - 3, x is now 12
x *= 2;  // Same as x = x * 2, x is now 24
x /= 4;  // Same as x = x / 4, x is now 6

// Comparison operators
console.log(5 == "5");   // true (value equality)
console.log(5 === "5");  // false (strict equality - different types)
console.log(5 != "5");   // false
console.log(5 !== "5");  // true
console.log(10 > 5);     // true
console.log(10 < 5);     // false

// Logical operators
let isAdult = true;
let hasPermission = false;
console.log(isAdult && hasPermission); // false (both must be true)
console.log(isAdult || hasPermission); // true (at least one is true)
console.log(!isAdult); // false (negation)
Result
true
false
false
true
true
false
false
true
false

Control Flow

Control flow statements determine the order in which code is executed. They allow you to make decisions, create loops, and handle exceptions.

Conditional Statements

  • if statement: Executes a block of code if a condition is true
  • else statement: Executes a block of code if the same condition is false
  • else if statement: Specifies a new condition if the first condition is false
  • switch statement: Selects one of many code blocks to be executed

Loops

  • for loop: Repeats a block of code a specified number of times
  • while loop: Repeats a block of code while a condition is true
  • do...while loop: Repeats a block of code while a condition is true (executes at least once)
  • for...in loop: Iterates over properties of an object
  • for...of loop: Iterates over values of an iterable object (arrays, strings, etc.)

Error Handling

  • try...catch statement: Handles errors gracefully without stopping the script
  • throw statement: Creates custom errors
  • finally block: Executes code regardless of the result
Example
// If, else if, else statement
let hour = 15;
let greeting;

if (hour < 12) {
  greeting = "Good morning";
} else if (hour < 18) {
  greeting = "Good afternoon";
} else {
  greeting = "Good evening";
}

console.log(greeting); // "Good afternoon"

// Switch statement
let day = 3;
let dayName;

switch (day) {
  case 1:
    dayName = "Monday";
    break;
  case 2:
    dayName = "Tuesday";
    break;
  case 3:
    dayName = "Wednesday";
    break;
  case 4:
    dayName = "Thursday";
    break;
  case 5:
    dayName = "Friday";
    break;
  default:
    dayName = "Weekend";
}

console.log(dayName); // "Wednesday"

// For loop
console.log("For loop:")
for (let i = 0; i < 3; i++) {
  console.log(i);
}

// While loop
console.log("While loop:")
let j = 0;
while (j < 3) {
  console.log(j);
  j++;
}

// Do-while loop
console.log("Do-while loop:")
let k = 0;
do {
  console.log(k);
  k++;
} while (k < 3);

// Error handling
try {
  // Attempt to execute this code
  let result = undefinedVariable / 10; // This will cause an error
} catch (error) {
  console.log("An error occurred: " + error.message);
} finally {
  console.log("This always executes");
}
Result
Good afternoon
Wednesday
For loop:
0
1
2
While loop:
0
1
2
Do-while loop:
0
1
2
An error occurred: undefinedVariable is not defined
This always executes

Functions and Objects

Functions

Functions are blocks of code designed to perform a particular task. They are executed when they are called (invoked).

Function Declaration

There are several ways to define functions in JavaScript:

  • Function Declaration: Uses the function keyword followed by a name
  • Function Expression: Assigns an anonymous function to a variable
  • Arrow Functions (ES6): A shorter syntax using the => symbol

Parameters and Arguments

Functions can take inputs (parameters) and return outputs. Parameters are listed in the function definition, while arguments are the values passed to the function when it's called.

Scope and Closures

Each function creates its own scope, which is the context where variables are accessible. A closure is a function that remembers its outer variables and can access them later, even when executed outside its original scope.

Example
// Function Declaration
function greet(name) {
  return 'Hello, ' + name + '!';
}

// Function Expression
const sayGoodbye = function(name) {
  return 'Goodbye, ' + name + '!';
};

// Arrow Function (ES6)
const multiply = (a, b) => a * b;

// Calling functions
console.log(greet('John')); // "Hello, John!"
console.log(sayGoodbye('John')); // "Goodbye, John!"
console.log(multiply(5, 3)); // 15

// Default parameters (ES6)
function greetWithDefault(name = 'Guest') {
  return 'Hello, ' + name + '!';
}

console.log(greetWithDefault()); // "Hello, Guest!"

// Rest parameters (ES6)
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3, 4, 5)); // 15

// Closures
function createCounter() {
  let count = 0;
  
  return function() {
    count += 1;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Result
Hello, John!
Goodbye, John!
15
Hello, Guest!
15
1
2
3

Objects

Objects are collections of key-value pairs, where the keys are strings (or Symbols) and the values can be any data type, including other objects and functions.

Creating Objects

There are several ways to create objects:

  • Object literals: Using curly braces {}
  • Constructor functions: Using new keyword
  • Object.create(): Using a prototype object
  • ES6 Classes: A more familiar syntax for object-oriented programming

Accessing Object Properties

You can access object properties using:

  • Dot notation: object.property
  • Bracket notation: object['property']

Object Methods

Functions stored as object properties are called methods. They can access and modify the object's properties using the this keyword.

Example
// Object literal
const person = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30,
  fullName: function() {
    return this.firstName + ' ' + this.lastName;
  }
};

// Accessing properties
console.log(person.firstName); // "John"
console.log(person['lastName']); // "Doe"

// Calling methods
console.log(person.fullName()); // "John Doe"

// Adding and modifying properties
person.email = 'john@example.com';
person.age = 31;

// Object.keys(), Object.values(), Object.entries()
console.log(Object.keys(person)); // ["firstName", "lastName", "age", "fullName", "email"]
console.log(Object.values(person)); // ["John", "Doe", 31, ƒ, "john@example.com"]

// ES6 enhanced object literals
const name = 'Computer';
const price = 1000;

const product = {
  name, // Same as name: name
  price, // Same as price: price
  displayInfo() { // Method shorthand
    return `${this.name}: $${this.price}`;
  }
};

console.log(product.displayInfo()); // "Computer: $1000"

// Constructor function
function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
  this.getDescription = function() {
    return `${this.year} ${this.make} ${this.model}`;
  };
}

const myCar = new Car('Toyota', 'Corolla', 2020);
console.log(myCar.getDescription()); // "2020 Toyota Corolla"

// ES6 Classes
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const john = new Person('John', 'Smith');
console.log(john.getFullName()); // "John Smith"
Result
John
Doe
John Doe
["firstName", "lastName", "age", "fullName", "email"]
Computer: $1000
2020 Toyota Corolla
John Smith

Arrays

Arrays are ordered collections of values. In JavaScript, arrays can hold mixed types of data and are dynamically sized.

Creating Arrays

You can create arrays using:

  • Array literals: []
  • Array constructor: new Array()

Array Methods

JavaScript arrays come with many built-in methods for manipulation:

  • Adding/Removing Elements: push(), pop(), shift(), unshift(), splice()
  • Finding Elements: indexOf(), find(), filter()
  • Transforming Arrays: map(), reduce(), sort(), reverse()
  • Iterating: forEach()
  • Other Useful Methods: concat(), slice(), join(), includes()
Example
// Creating arrays
let fruits = ['Apple', 'Banana', 'Orange'];
let mixed = [1, 'hello', true, {name: 'John'}, [1, 2, 3]];

// Accessing elements
console.log(fruits[0]); // "Apple"
console.log(fruits[fruits.length - 1]); // "Orange" (last element)

// Adding elements
fruits.push('Mango'); // Add to end
fruits.unshift('Strawberry'); // Add to beginning
console.log(fruits); // ["Strawberry", "Apple", "Banana", "Orange", "Mango"]

// Removing elements
let lastFruit = fruits.pop(); // Remove from end
let firstFruit = fruits.shift(); // Remove from beginning
console.log(fruits); // ["Apple", "Banana", "Orange"]
console.log(lastFruit); // "Mango"
console.log(firstFruit); // "Strawberry"

// Transforming arrays
let numbers = [1, 2, 3, 4, 5];

// map: Creates a new array by performing a function on each array element
let doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter: Creates a new array with elements that pass a test
let evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]

// reduce: Reduces the array to a single value
let sum = numbers.reduce((total, num) => total + num, 0);
console.log(sum); // 15

// forEach: Executes a function for each array element
numbers.forEach(num => console.log(num));

// find: Returns the first element that passes a test
let found = numbers.find(num => num > 3);
console.log(found); // 4

// sort: Sorts the elements of an array
let unsortedNumbers = [3, 1, 4, 1, 5];
unsortedNumbers.sort();
console.log(unsortedNumbers); // [1, 1, 3, 4, 5]

// Custom sort function
let scores = [40, 100, 1, 5, 25, 10];
scores.sort((a, b) => a - b); // Ascending order
console.log(scores); // [1, 5, 10, 25, 40, 100]
Result
Apple
Orange
["Strawberry", "Apple", "Banana", "Orange", "Mango"]
["Apple", "Banana", "Orange"]
Mango
Strawberry
[2, 4, 6, 8, 10]
[2, 4]
15
1
2
3
4
5
4
[1, 1, 3, 4, 5]
[1, 5, 10, 25, 40, 100]

DOM Manipulation

What is the DOM?

The Document Object Model (DOM) is a programming interface for HTML and XML documents. It represents the structure of a document as a tree of objects, where each object corresponds to a part of the document.

The DOM provides a way for JavaScript to:

  • Access and change document content, structure, and styles
  • Add, modify, or delete HTML elements and attributes
  • React to events like clicks, form submissions, etc.

The DOM is not a part of JavaScript; it's a Web API that browsers implement to allow scripts to interact with the web page.

Example
// The document object is the entry point to the DOM
console.log(document.title); // Gets the page title

// DOM navigation
console.log(document.body); // The body element
console.log(document.head); // The head element
console.log(document.documentElement); // The html element

// Basic DOM structure example
/*
<html> (document.documentElement)
  <head> (document.head)
    <title>My Page</title>
  </head>
  <body> (document.body)
    <h1 id="title">Hello World</h1>
    <p class="content">This is a paragraph.</p>
  </body>
</html>
*/

// Parent, children, and siblings
const body = document.body;
const firstChild = body.firstChild; // First child node
const firstElement = body.firstElementChild; // First element node
const childNodes = body.childNodes; // All child nodes
const children = body.children; // All element nodes

// DOM node types
// Element nodes: Regular HTML elements (type 1)
// Text nodes: Text content (type 3)
// Comment nodes: HTML comments (type 8)
// Document nodes: The document itself (type 9)

Selecting DOM Elements

JavaScript provides several methods to select elements from the DOM:

Basic Selectors

  • getElementById(): Selects an element by its ID
  • getElementsByTagName(): Selects all elements with the given tag name
  • getElementsByClassName(): Selects all elements with the given class name

Query Selectors (More Powerful)

  • querySelector(): Returns the first element that matches a CSS selector
  • querySelectorAll(): Returns all elements that match a CSS selector

Query selectors use CSS-style syntax, making them very versatile for complex selections.

Example
// Assuming we have this HTML:
/*
<div id="container">
  <h1 id="title">My Page</h1>
  <p class="content">First paragraph</p>
  <p class="content">Second paragraph</p>
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>
</div>
*/

// getElementById
const title = document.getElementById('title');
console.log(title); // <h1 id="title">My Page</h1>

// getElementsByTagName
const paragraphs = document.getElementsByTagName('p');
console.log(paragraphs.length); // 2 (HTMLCollection of 2 items)

// getElementsByClassName
const contentElements = document.getElementsByClassName('content');
console.log(contentElements.length); // 2 (HTMLCollection of 2 items)

// querySelector - returns the first match
const firstParagraph = document.querySelector('p');
console.log(firstParagraph); // <p class="content">First paragraph</p>

// querySelectorAll - returns all matches
const allParagraphs = document.querySelectorAll('p');
console.log(allParagraphs.length); // 2 (NodeList of 2 items)

// Complex CSS selectors
const listItems = document.querySelectorAll('ul li');
const secondContentParagraph = document.querySelector('p.content:nth-child(3)');
const containedElements = document.querySelectorAll('#container > *');

// Converting collections to arrays
// HTMLCollection and NodeList are array-like, but not true arrays
const paragraphArray = Array.from(paragraphs);
// Or using spread syntax
const listItemArray = [...listItems];

// Now we can use array methods
paragraphArray.forEach(p => {
  console.log(p.textContent);
});

Modifying the DOM

After selecting elements, you can modify them in various ways:

Changing Content

  • textContent: Gets or sets the text content (without HTML)
  • innerHTML: Gets or sets the HTML content (including tags)
  • innerText: Gets or sets the visible text content

Modifying Attributes

  • getAttribute(): Gets the value of an attribute
  • setAttribute(): Sets the value of an attribute
  • removeAttribute(): Removes an attribute
  • Direct property access: element.id, element.src, etc.

Manipulating Classes

  • classList.add(): Adds a class
  • classList.remove(): Removes a class
  • classList.toggle(): Toggles a class on/off
  • classList.contains(): Checks if an element has a class

Changing Styles

  • style property: Directly modify inline styles
  • getComputedStyle(): Get applied styles (read-only)

Creating and Removing Elements

  • createElement(): Creates a new element
  • appendChild(): Adds a child element
  • insertBefore(): Inserts before another element
  • removeChild(): Removes a child element
  • remove(): Removes the element itself
Example
// Changing content
const title = document.querySelector('#title');
title.textContent = 'New Title'; // Changes text without HTML
title.innerHTML = 'New <em>Emphasized</em> Title'; // Parses and applies HTML

// Modifying attributes
const link = document.querySelector('a');
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');

// Shortcuts for common attributes
link.href = 'https://example.com';
link.target = '_blank';

// Manipulating classes
const paragraph = document.querySelector('p');
paragraph.classList.add('highlight'); // Adds a class
paragraph.classList.remove('old-class'); // Removes a class
paragraph.classList.toggle('active'); // Adds if absent, removes if present
const hasClass = paragraph.classList.contains('highlight'); // true

// Changing styles
paragraph.style.color = 'blue';
paragraph.style.backgroundColor = 'yellow';
paragraph.style.padding = '10px';

// Creating and adding elements
const newElement = document.createElement('div');
newElement.textContent = 'This is a new element';
newElement.className = 'new-div';

const container = document.querySelector('#container');
container.appendChild(newElement); // Adds at the end

// Insert before another element
const firstParagraph = document.querySelector('p');
const newParagraph = document.createElement('p');
newParagraph.textContent = 'Inserted paragraph';
container.insertBefore(newParagraph, firstParagraph);

// Removing elements
container.removeChild(newParagraph); // Remove specific child
// Or using the newer method
newElement.remove(); // Remove the element itself
Result

New Emphasized Title

This is a paragraph.

Events

Events are actions or occurrences that happen in the browser, which can be detected and responded to with JavaScript. Common events include clicks, keyboard presses, form submissions, page loads, and more.

Adding Event Listeners

There are several ways to add event listeners:

  • addEventListener(): The modern, recommended way
  • HTML attribute: onclick="handler()" (not recommended)
  • DOM property: element.onclick = function() {} (limited to one handler)

Common Events

  • Mouse events: click, dblclick, mouseenter, mouseleave, mouseover, mouseout, mousemove
  • Keyboard events: keydown, keyup, keypress
  • Form events: submit, reset, change, input, focus, blur
  • Document/Window events: load, resize, scroll, DOMContentLoaded

The Event Object

When an event occurs, the browser creates an event object with details about the event. This object is automatically passed to event handlers.

Example
// Adding event listeners
const button = document.querySelector('#myButton');

// Method 1: addEventListener (recommended)
button.addEventListener('click', function(event) {
  console.log('Button clicked!');
  console.log(event); // The event object
});

// Method 2: DOM property (limited to one handler per event)
button.onclick = function() {
  alert('Button clicked!');
};

// Method 3: HTML attribute (not recommended)
// <button onclick="alert('Button clicked!')">Click me</button>

// Event object properties
button.addEventListener('click', function(event) {
  // Common properties
  console.log(event.type); // "click"
  console.log(event.target); // The element that triggered the event
  console.log(event.currentTarget); // The element that the listener is attached to
  console.log(event.timeStamp); // When the event occurred
  
  // Mouse event properties
  console.log(event.clientX, event.clientY); // Mouse position relative to viewport
  console.log(event.button); // Which mouse button was pressed
});

// Prevent default behavior
const link = document.querySelector('a');
link.addEventListener('click', function(event) {
  event.preventDefault(); // Prevents navigating to the link
  console.log('Link click prevented');
});

// Stop propagation (event bubbling)
const child = document.querySelector('.child');
child.addEventListener('click', function(event) {
  event.stopPropagation(); // Prevents the event from bubbling up
  console.log('Child clicked, parent will not be notified');
});

// Event delegation (efficient for many similar elements)
const list = document.querySelector('ul');
list.addEventListener('click', function(event) {
  // Check if a list item was clicked
  if (event.target.tagName === 'LI') {
    console.log('List item clicked:', event.target.textContent);
  }
});

// Common events examples

// Form submission
const form = document.querySelector('form');
form.addEventListener('submit', function(event) {
  event.preventDefault(); // Prevent actual form submission
  console.log('Form submitted');
  // Get form data
  const formData = new FormData(form);
  for (let [key, value] of formData.entries()) {
    console.log(key, value);
  }
});

// Keyboard events
document.addEventListener('keydown', function(event) {
  console.log('Key pressed:', event.key);
  // Check for specific keys
  if (event.key === 'Escape') {
    console.log('Escape key pressed');
  }
});

// Window events
window.addEventListener('resize', function() {
  console.log('Window resized to:', window.innerWidth, 'x', window.innerHeight);
});

// DOMContentLoaded - when the HTML is fully loaded
document.addEventListener('DOMContentLoaded', function() {
  console.log('DOM fully loaded');
});

Asynchronous JavaScript

Introduction to Asynchronous JS

JavaScript is primarily single-threaded, which means it can only execute one piece of code at a time. However, many operations like fetching data from servers, reading files, or waiting for user input take time to complete.

Asynchronous programming is a technique that enables your program to start a potentially long-running task and still be able to be responsive to other events while that task runs, rather than having to wait until that task has finished.

Key Asynchronous Concepts

  • Callbacks: Functions passed as arguments to be executed after another function completes
  • Promises: Objects representing the eventual completion or failure of an asynchronous operation
  • Async/Await: A newer, more readable way to work with promises
  • Event Loop: JavaScript's mechanism for handling asynchronous operations
Example
// Synchronous code (blocking)
console.log('Start');
function calculateSum() {
  let sum = 0;
  for (let i = 0; i < 1000000000; i++) {
    sum += i;
  }
  return sum;
}
// This blocks the execution until completed
// console.log('Sum:', calculateSum());
console.log('End');

// Asynchronous code (non-blocking)
console.log('Start');
setTimeout(() => {
  console.log('This runs after 2 seconds');
}, 2000);
console.log('End');

// The event loop
console.log('First'); // Executes immediately (call stack)

setTimeout(() => {
  console.log('Second'); // Executes after the specified delay (callback queue)
}, 0);

Promise.resolve().then(() => {
  console.log('Third'); // Executes after synchronous code but before timeout (microtask queue)
});

console.log('Fourth'); // Executes immediately (call stack)

// Output will be: First, Fourth, Third, Second
Result
Start
End
Start
End
First
Fourth
Third
Second
This runs after 2 seconds

Callbacks

A callback is a function passed as an argument to another function, which is then invoked inside the outer function to complete some kind of action. Callbacks are the oldest way of handling asynchronous operations in JavaScript.

How Callbacks Work

  • A function (let's call it 'A') is passed as an argument to another function (let's call it 'B')
  • Function B executes some operations, and when it finishes, it calls function A
  • This allows function B to notify function A when it's done, even if B's work takes time

Callback Hell

When multiple asynchronous operations depend on each other, callbacks can lead to deeply nested code that's hard to read and maintain. This is often called "callback hell" or the "pyramid of doom".

Example
// Basic callback example
function greet(name, callback) {
  console.log('Hello, ' + name);
  callback();
}

greet('John', function() {
  console.log('Callback function executed!');
});

// Asynchronous callback with setTimeout
console.log('Starting...');

setTimeout(function() {
  console.log('This runs after 2 seconds');
}, 2000);

console.log('Continuing execution...');

// Real-world example: loading a script
function loadScript(src, callback) {
  const script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));
  document.head.append(script);
}

// Using the loadScript function
loadScript('https://example.com/script.js', function(error, script) {
  if (error) {
    console.error('Error loading the script:', error);
  } else {
    console.log('Script loaded successfully:', script.src);
    // Maybe load another script...
  }
});

// Callback Hell example
function processData() {
  fetchData(function(data) {
    processStep1(data, function(result1) {
      processStep2(result1, function(result2) {
        processStep3(result2, function(result3) {
          processStep4(result3, function(result4) {
            console.log('Final result:', result4);
          }, handleError);
        }, handleError);
      }, handleError);
    }, handleError);
  }, handleError);
}

function handleError(error) {
  console.error('An error occurred:', error);
}
Result
Hello, John
Callback function executed!
Starting...
Continuing execution...
This runs after 2 seconds

Promises

Promises were introduced in ES6 (2015) to handle asynchronous operations in a more elegant way than callbacks. A Promise is an object representing the eventual completion or failure of an asynchronous operation.

Promise States

A Promise can be in one of these states:

  • Pending: Initial state, neither fulfilled nor rejected
  • Fulfilled: The operation completed successfully
  • Rejected: The operation failed

Promise Methods

  • then(): Called when the promise is fulfilled
  • catch(): Called when the promise is rejected
  • finally(): Called regardless of success or failure

Promise Chaining

One of the key advantages of promises is the ability to chain multiple asynchronous operations together in a readable way.

Promise Static Methods

  • Promise.all(): Waits for all promises to resolve
  • Promise.race(): Waits for the first promise to resolve or reject
  • Promise.resolve(): Returns a resolved promise
  • Promise.reject(): Returns a rejected promise
Example
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  // Simulating an asynchronous operation
  const success = true;
  
  setTimeout(() => {
    if (success) {
      resolve('Operation successful!'); // Fulfilled
    } else {
      reject(new Error('Operation failed!')); // Rejected
    }
  }, 1000);
});

// Using a Promise
myPromise
  .then((result) => {
    console.log('Success:', result);
  })
  .catch((error) => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log('Promise settled (fulfilled or rejected)');
  });

// Promise chaining
function fetchUserData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 1, name: 'John' });
    }, 1000);
  });
}

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

fetchUserData()
  .then((user) => {
    console.log('User:', user);
    return fetchUserPosts(user.id);
  })
  .then((posts) => {
    console.log('Posts:', posts);
  })
  .catch((error) => {
    console.error('Error:', error);
  });

// Promise.all - wait for multiple promises
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve) => setTimeout(() => resolve('foo'), 1000));
const promise3 = fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(response => response.json());

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log('All promises resolved:', values);
  })
  .catch((error) => {
    console.error('At least one promise rejected:', error);
  });

// Promise.race - first to settle wins
const fastPromise = new Promise((resolve) => setTimeout(() => resolve('fast'), 100));
const slowPromise = new Promise((resolve) => setTimeout(() => resolve('slow'), 500));

Promise.race([fastPromise, slowPromise])
  .then((result) => {
    console.log('Fastest promise result:', result); // 'fast'
  });
Result
Success: Operation successful!
Promise settled (fulfilled or rejected)
User: {id: 1, name: 'John'}
Posts: ['Post 1', 'Post 2', 'Post 3']
Fastest promise result: fast
All promises resolved: [3, 'foo', {userId: 1, id: 1, title: '...', completed: false}]

Async/Await

Async/await, introduced in ES2017 (ES8), is syntactic sugar built on top of Promises, making asynchronous code look and behave more like synchronous code. This makes asynchronous code more readable and easier to debug.

Key Components

  • async: Keyword used to declare an asynchronous function that automatically returns a Promise
  • await: Keyword that pauses the execution of an async function until a Promise is settled

Benefits of Async/Await

  • Cleaner, more readable code than promise chains
  • Better error handling with try/catch blocks
  • Easier debugging

Limitations

  • Can only be used inside async functions
  • Still uses Promises under the hood
Example
// Basic async/await
async function fetchData() {
  return 'Data fetched!';
}

// This is equivalent to:
// function fetchData() {
//   return Promise.resolve('Data fetched!');
// }

// Using an async function
fetchData().then(data => console.log(data));

// Using await inside an async function
async function displayData() {
  try {
    const data = await fetchData();
    console.log('Received:', data);
  } catch (error) {
    console.error('Error:', error);
  }
}

displayData();

// Real-world example: fetching data from an API
async function fetchUserData(userId) {
  try {
    // await pauses execution until the Promise resolves
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
    
    // Check if the request was successful
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    
    // Parse the JSON response
    const userData = await response.json();
    return userData;
  } catch (error) {
    console.error('Error fetching user data:', error);
    throw error; // Re-throw to allow caller to handle
  }
}

// Using the async function
async function displayUser() {
  try {
    const user = await fetchUserData(1);
    console.log('User data:', user);
  } catch (error) {
    console.error('Failed to display user:', error);
  }
}

displayUser();

// Sequential vs Parallel execution

// Sequential (each await waits for the previous to complete)
async function sequentialFetch() {
  console.time('sequential');
  const user1 = await fetchUserData(1);
  const user2 = await fetchUserData(2);
  const user3 = await fetchUserData(3);
  console.timeEnd('sequential');
  return [user1, user2, user3];
}

// Parallel (all fetches start at the same time)
async function parallelFetch() {
  console.time('parallel');
  const userPromises = [
    fetchUserData(1),
    fetchUserData(2),
    fetchUserData(3)
  ];
  
  // Wait for all promises to resolve
  const users = await Promise.all(userPromises);
  console.timeEnd('parallel');
  return users;
}

// For loop with await
async function processItems(items) {
  const results = [];
  
  for (const item of items) {
    // Process items sequentially
    const result = await processItem(item);
    results.push(result);
  }
  
  return results;
}

// Handling errors
async function fetchWithErrorHandling() {
  try {
    const result = await fetchDataThatMightFail();
    return result;
  } catch (error) {
    console.error('Error in fetchWithErrorHandling:', error);
    // Handle error or rethrow
    throw error;
  } finally {
    // This runs regardless of success or failure
    console.log('Fetch operation completed');
  }
}
Result
Data fetched!
Received: Data fetched!
User data: {id: 1, name: 'Leanne Graham', ...}