Most interesting examples from MDN and LearnJS

Async Generators

This example demonstrates how to use an async function* (asynchronous generator) to yield values over time with a delay. The generateSequence function yields numbers from start to end, waiting one second between each using await.

The for await...of loop is used to consume the generator asynchronously, making it perfect for streaming data, timers, or controlled iteration over async events.

async function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    yield i;
  }
}
 
const timer = async (callback) => {
  const generator = generateSequence(1, 5);
  for await (let value of generator) {
    callback(value);
  }
};
 
timer(console.log).catch(console.error);
// 1, 2, 3, 4, 5 (with delay between)

bind

This example demonstrates how the bind method is used to fix the this context of a function.

In JavaScript, methods like setTimeout lose the original object context when passing a function reference. Using bind, we can permanently associate a method with its object (user), ensuring this.firstName refers to the correct value.

This is useful when passing object methods as callbacks, especially in asynchronous operations.

const user = {
  firstName: "John",
  sayHi() {
    console.log(`Hello, ${this.firstName}!`);
  },
};
 
user.sayHi(); // Hello, John!
setTimeout(user.sayHi, 0); // Hello, undefined!
 
// solution 1
setTimeout(function () {
  user.sayHi(); // Hello, John!
}, 0);
// or shorter
setTimeout(() => user.sayHi(), 0); // Hello, John!
 
// solution 2
const sayHi = user.sayHi.bind(user);
// can run it without an object
sayHi(); // Hello, John!
setTimeout(sayHi, 0); // Hello, John!

Delay Decorator

This example defines a delay decorator that wraps a function and delays its execution by a specified number of milliseconds.

It preserves the correct this context using Function.prototype.apply, allowing methods from different objects to be delayed without losing their binding. Useful for debouncing, throttling, or deferring side effects.

const obj1 = {
  info: "obj1 info",
  showInfo(...args) {
    console.log(`${args}: ${this.info}`);
  },
};
 
const obj2 = {
  info: "obj2 info",
};
 
function delay(f, ms) {
  return function (...args) {
    setTimeout(() => {
      // Uses apply to handle `this` dynamically
      f.apply(this, args);
    }, ms);
  };
}
 
obj1.delayedShowInfo = delay(obj1.showInfo, 1000);
obj2.delayedShowInfo = delay(obj1.showInfo, 1000);
 
obj1.delayedShowInfo("Info", "one"); // Info,one: obj1 info
obj2.delayedShowInfo("Info"); // Info: obj2 info

Delaying function execution with and without context (this)

This example shows how to create a delay utility that defers a function's execution by a specified time using setTimeout. It works great for standalone functions but does not preserve this context when used with object methods.

For object methods, Function.prototype.bind or apply should be used to retain proper context.

function delay(f, ms) {
  return function (...args) {
    setTimeout(() => {
      f(...args);
    }, ms);
  };
}
 
function showDetails(name, age) {
  console.log(`Name: ${name}, Age: ${age}`);
}
 
const delayedShowDetails = delay(showDetails, 1000);
delayedShowDetails("Alice", 25);
// Name: Alice, Age: 25
 
const obj1 = {
  info: "obj1 info",
  showInfo(prefix) {
    console.log(`${prefix}: ${this?.info}`);
  },
};
 
obj1.delayedShowInfo = delay(obj1.showInfo, 1000);
obj1.delayedShowInfo("Info", "one"); // Info: undefined

Event Loop

JavaScript’s event loop is the mechanism that handles asynchronous operations by managing the call stack, microtask queue, and macrotask queue. This section includes practical examples demonstrating how Promises, setTimeout, async/await, and synchronous code interact in different phases of the event loop.

Key Concepts Covered:

Why This Matters:

Mastering the event loop helps developers avoid performance bottlenecks, race conditions, and timing bugs. It’s also a must-know topic for technical interviews and real-time app development.

🧵 Promise.all and the Event Loop

Promise.all resolves when all input promises (or values) are fulfilled. Non-promise values are treated as already resolved, but their evaluation still runs asynchronously. This snippet also shows how setTimeout helps observe the order of event loop execution and when the microtask queue is cleared.

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "foo");
});
const promise3 = 42;
 
Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log({ values });
});
 
// Using setTimeout, we can execute code after the queue is empty
setTimeout(() => {
  console.log("the queue is now empty");
});
 
const p3 = Promise.all([]); // Will be immediately resolved
const p4 = Promise.all([1337, "hi"]);
 
// Non-promise values are ignored, but the evaluation is done asynchronously
console.log({ p3 });
console.log({ p4 });
 
setTimeout(() => {
  console.log({ p4 });
});
 
Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log({ values2: values });
});
 
const promise4 = Promise.resolve(3);
const promise5 = 42;
 
Promise.all([promise4, promise5]).then((values) => {
  console.log({ values3: values });
});
 
// { p3: Promise { [] } }
// { p4: Promise { <pending> } }
// { values3: [ 3, 42 ] }
// the queue is now empty
// { p4: Promise { [ 1337, 'hi' ] } }
// { values: [ 3, 'foo', 42 ] }
// { values2: [ 3, 'foo', 42 ] }

🔄 Promise Chaining and Microtask Queue Order

Even though promise1 and promise2 are resolved immediately, their .then() callbacks are placed in the microtask queue. Microtasks are executed in the order they’re queued, and each .then() forms its own chain — leading to interleaved output.

const promise1 = Promise.resolve();
const promise2 = Promise.resolve();
 
promise1.then(() => console.log(1)).then(() => console.log(2));
promise2.then(() => console.log(3)).then(() => console.log(4));
// 1 3 2 4

⏲️ let in Loops with setTimeout

Using let in a loop ensures that each iteration captures its own block-scoped value of i. All setTimeout callbacks execute after 1 second, printing 0 to 3 as expected — thanks to let's scoping behavior.

for (let i = 0; i < 4; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
} // 0, 1, 2, 3. All after 1s

🔁 Promise Lifecycle and Event Loop Timing

This example shows the synchronous and asynchronous parts of a Promise's lifecycle. The executor function runs immediately, while .then() handlers are queued as microtasks and executed after the current call stack clears — before any setTimeout callbacks.

const promise = new Promise((resolve, reject) => {
  console.log("Promise callback");
  resolve("resolved");
  console.log("Promise callback end");
}).then((result) => {
  console.log("Promise callback (.then)", result);
});
 
setTimeout(() => {
  console.log("event-loop cycle: Promise (fulfilled)", promise);
}, 0);
 
console.log("Promise (pending)", promise);
 
// Promise callback
// Promise callback end
// Promise (pending) Promise { <pending> }
// Promise callback (.then) resolved
// event-loop cycle: Promise (fulfilled) Promise { undefined }

⚙️ Async Function and Timer Execution Order

Even when using await, async functions execute their synchronous parts immediately. This example highlights how setTimeout callbacks are deferred to the macrotask queue, while synchronous code inside async functions runs first.

async function run() {
  console.log("run async");
  setTimeout(() => {
    console.log("run timeout");
  }, 0);
}
 
setTimeout(() => {
  console.log("timeout");
}, 0);
 
// await or not, same result
await run();
 
console.log("script");
 
// run async
// script
// timeout
// run timeout

⏳ Blocking the Event Loop with a While Loop

This snippet demonstrates that synchronous code (a busy while loop) can block the event loop. Even though setTimeout is set to execute after 500ms, the callback is delayed until the loop finishes—after roughly 2 seconds.

const seconds = new Date().getTime() / 1000;
 
setTimeout(() => {
  // prints out "2", meaning that the callback is not called immediately after 500 milliseconds.
  console.log(`Ran after ${new Date().getTime() / 1000 - seconds} seconds`);
}, 500);
 
while (true) {
  if (new Date().getTime() / 1000 - seconds >= 2) {
    console.log("Good, looped for 2 seconds");
    break;
  }
}
 
// Good, looped for 2 seconds
// Ran after 2.01 seconds

📜 Script, Microtasks, and Macrotasks in Execution Order

This example demonstrates how JavaScript prioritizes synchronous code first, followed by microtasks (.then, queueMicrotask), and finally macrotasks (setTimeout). It reveals the precise order in which the event loop processes each queue.

console.log("Script start");
 
setTimeout(() => {
  console.log("setTimeout");
}, 0);
 
Promise.resolve()
  .then(() => {
    console.log("Promise 1");
  })
  .then(() => {
    console.log("Promise 2");
  });
 
console.log("Script end");
 
const promise1 = new Promise((resolve, reject) => {
  console.log("Promise constructor");
  resolve();
}).then(() => {
  console.log("Promise constructor resolve");
});
 
queueMicrotask(() => {
  console.log("Microtask queue");
});
 
console.log("After Promise constructor");
 
// Script start
// Script end
// Promise constructor
// After Promise constructor
// Promise 1
// Promise constructor resolve
// Microtask queue
// Promise 2
// setTimeout

🕒 Blocking Inside Async Callbacks

Even though the task is scheduled with setTimeout, once it begins, the long-running synchronous task blocks the event loop. This illustrates how JavaScript remains single-threaded—even asynchronous calls can't interrupt blocking operations.

function longRunningTask() {
  console.log("Start Long-Running Task");
 
  const startTime = Date.now();
  while (Date.now() - startTime < 2000) {
    // Simulate a long-running task (3 seconds)
  }
 
  console.log("Long-Running Task Completed");
}
 
function simulateNonBlocking() {
  console.log("Start");
 
  setTimeout(() => {
    console.log("Non-blocking Operation");
    longRunningTask();
  }, 0);
 
  console.log("End");
}
 
simulateNonBlocking();
 
// Start
// End
// Non-blocking Operation
// Start Long-Running Task
// after 2s:
// Long-Running Task Completed

🧬 Nested Microtasks in Macrotasks

Microtasks (like .then()) always run before macrotasks (setTimeout). Even when a Promise is placed inside a setTimeout, its callback becomes a microtask and runs immediately after the current macrotask finishes.

console.log("Start");
 
setTimeout(() => {
  console.log("setTimeout 1");
  Promise.resolve().then(() => {
    console.log("Promise inside setTimeout 1");
  });
}, 0);
 
setTimeout(() => {
  console.log("setTimeout 2");
}, 0);
 
Promise.resolve()
  .then(() => {
    console.log("Promise 1");
  })
  .then(() => {
    console.log("Promise 2");
  });
 
console.log("End");
 
// Start
// End
// Promise 1
// Promise 2
// setTimeout 1
// Promise inside setTimeout 1
// setTimeout 2

🎥 requestAnimationFrame and Task Ordering

This example shows how different task types (setTimeout, Promises, requestAnimationFrame) are prioritized. Microtasks (.then) run before macrotasks (setTimeout), and requestAnimationFrame is queued to run right before the next paint — after all other queues are cleared.

console.log("1");
 
setTimeout(function () {
  console.log("2");
 
  Promise.resolve().then(function () {
    console.log("3");
  });
}, 0);
 
Promise.resolve().then(function () {
  console.log("4");
 
  setTimeout(function () {
    console.log("5");
  }, 0);
});
 
requestAnimationFrame(function () {
  console.log("7");
});
 
console.log("6");
 
// 1, 6, 4, 2, 3, 5, 7

Fibonacci

The Fibonacci sequence is a series where each number is the sum of the two preceding ones: 1, 1, 2, 3, 5, 8, 13, 21, ...

This example shows two ways to calculate the n-th Fibonacci number:

🔁 Iterative Approach

Efficient and suitable for large values of n (like 77). Uses a loop and keeps track of the last two values.

function fib(n) {
  let a = 1;
  let b = 1;
  for (let i = 3; i <= n; i++) {
    let c = a + b;
    a = b;
    b = c;
  }
  return b;
}
 
console.log(fib(3)); // 2
console.log(fib(7)); // 13
console.log(fib(77)); // 5527939700884757

🔁 Recursive Approach

Elegant but inefficient for large n due to repeated calculations (exponential time complexity). Best for learning recursion.

function fib(n) {
  return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
 
console.log(fib(3)); // 2
console.log(fib(7)); // 13
console.log(fib(77)); // -

for and while

This example highlights the subtle differences between ++i (pre-increment) and i++ (post-increment) in while loops, and how they affect loop behavior and output.

It also compares for loops using i++ vs ++i, which behave the same in this context, since the increment happens after the loop body executes.

let i = 0;
while (++i < 3) console.log(i);
// 1, 2
 
let i2 = 0;
while (i2++ < 3) console.log(i2);
// 1, 2, 3
 
for (let i = 0; i < 3; i++) console.log(i);
// 0, 1, 2
 
for (let i = 0; i < 3; ++i) console.log(i);
// 0, 1, 2

Memoize

Memoization is an optimization technique that caches the result of function calls based on their input arguments. If the same inputs occur again, the cached result is returned instead of recalculating.

This example wraps a basic function (adder) with a memoize utility using a Map and JSON.stringify to store and retrieve results by argument key.

function memoize(fn) {
  const cache = new Map();
 
  return function (...args) {
    const key = JSON.stringify(args);
 
    if (cache.has(key)) {
      return cache.get(key);
    }
 
    const result = fn.apply(this, args);
    cache.set(key, result);
 
    return result;
  };
}
 
const adder = function (a, b) {
  console.log(`Calculating ${a} + ${b}`);
  return a + b;
};
 
const memoizedAdder = memoize(adder);
const result1 = memoizedAdder(3, 4); // Calculating 3 + 4
console.log(result1); // 7
const result2 = memoizedAdder(3, 4);
console.log(result2); // 7

Mixins

Mixins allow objects to share reusable behavior without using classical inheritance. In this example, the CanSpeak and CanWalk mixins are applied to different classes using Object.assign, enabling both Person and Robot to share functionality like speak and walk.

mixins

// Define a mixin for shared behavior
const CanSpeak = {
  speak() {
    console.log(`${this.name} says: ${this.message}`);
  },
};
 
// Define another mixin
const CanWalk = {
  walk() {
    console.log(`${this.name} is walking.`);
  },
};
 
// Create a base class
class Person {
  constructor(name, message) {
    this.name = name;
    this.message = message;
  }
}
 
// Apply mixins to the class
Object.assign(Person.prototype, CanSpeak, CanWalk);
 
// Create an instance of the class
const john = new Person('John', 'Hello, world!');
 
// Use methods from mixins
john.speak(); // Output: John says: Hello, world!
john.walk();  // Output: John is walking.
 
// Another example with a different class
class Robot {
  constructor(name, message) {
    this.name = name;
    this.message = message;
  }
}
 
// Apply mixins to the Robot class
Object.assign(Robot.prototype, CanSpeak, CanWalk);
 
const r2d2 = new Robot('R2D2', 'Beep boop');
r2d2.speak(); // Output: R2D2 says: Beep boop
r2d2.walk();  // Output: R2D2 is walking.

Object and Map Conversion

This example shows how to convert between plain JavaScript objects and Map instances using Object.entries() and Object.fromEntries().

These methods are useful when working with APIs or libraries that prefer one format over the other, or when you need key ordering and additional Map features.

const prices = Object.fromEntries([
  ["banana", 1],
  ["orange", 2],
  ["meat", 4],
]);
 
console.log(prices);
// { banana: 1, orange: 2, meat: 4 }
 
const map = new Map();
map.set("banana", 1);
map.set("orange", 2);
map.set("meat", 4);
 
const arrayLikeMapEntries = map.entries();
const arrayMapEntries = Array.from(arrayLikeMapEntries);
 
const objectFromMap = Object.fromEntries(arrayMapEntries);
console.log(objectFromMap);
// { banana: 1, orange: 2, meat: 4 }
 
const mapFromObject = new Map(Object.entries(objectFromMap));
console.log(mapFromObject.get("meat")); // 4

Object Methods

This collection of examples explores how this behaves in different contexts: regular functions, arrow functions, method calls, class constructors, and asynchronous functions like setTimeout.

It covers:

Understanding these patterns is essential for working with objects, classes, and asynchronous code in JavaScript.

object-methods

let user = {
  name: "John",
  age: 30,
 
  sayHi() {
    console.log(user.name); // leads to an error
  },
};
 
const admin = user;
user = null; // overwrite user object
 
try {
  admin.sayHi();
} catch (e) {
  console.error(e.message);
}
// Cannot read properties of null (reading 'name')
 
console.log(admin);
// { name: 'John', age: 30, sayHi: [Function: sayHi] }

❌ Accessing user directly inside a method

Avoid directly referencing the object (user) inside its own method. If the object is reassigned or removed, the method will throw an error. Use this instead to safely access object properties.

let user = {
  name: "John",
  age: 30,
 
  sayHi() {
    console.log(this.name);
  },
};
 
const admin = user;
user = null; // overwrite user object
 
try {
  admin.sayHi();
} catch (e) {
  console.error(e.message);
}
// John
 
console.log(admin);
// { name: 'John', age: 30, sayHi: [Function: sayHi] }

🔄 Arrow vs Regular Function and this

Arrow functions don’t have their own this — they inherit it from the surrounding scope. In contrast, regular functions define their own this, which can lead to unexpected undefined values when used as methods.

const user = {
  firstName: "John",
  sayHi() {
    const arrow = () => console.log(this.firstName);
    arrow();
  },
};
user.sayHi(); // John
 
const user2 = {
  firstName: "John",
  sayHi() {
    function normal() {
      console.log(this.firstName);
    }
    normal();
  },
};
user2.sayHi(); // undefined

⚠️ this in object literals vs methods

When returning an object from a function, this does not refer to the object being created — it refers to the function's execution context. To correctly reference the object itself, define ref as a method so that this is bound to the object at call time.

function makeUser() {
  return {
    name: "John",
    ref: this,
  };
}
const user = makeUser();
console.log(user.ref.name); // undefined
 
// The value of this is one for the whole function
// ... same as
function makeUser2() {
  return this; // this time there's no object literal
}
console.log(makeUser2().name); // undefined
 
function makeUser3() {
  return {
    name: "John",
    ref() {
      return this;
    },
  };
}
 
const user2 = makeUser3();
 
console.log(user2.ref().name); // John
 
// Now it works, because user2.ref() is a method.
// And the value of this is set to the object before dot

🪜 Method Chaining with this

By returning this from each method, you enable method chaining — a clean and expressive way to perform multiple operations on the same object in sequence.

const ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep: function () {
    // shows the current step
    console.log(this.step);
    return this;
  },
};
 
ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1
ladder.down();
ladder.showStep(); // 0
 
ladder.up().up().down().showStep().down().showStep();
// shows 1 then 0

🔍 this in Regular vs Arrow Methods

Regular functions bind this to the object they belong to, while arrow functions inherit this from the outer scope — which may lead to unexpected undefined or NaN values when used as methods.

const obj1 = {
  value: 5,
  regularMethod: function () {
    return this?.value + 10;
  },
  arrowMethod: () => {
    return this?.value + 20;
  },
};
 
console.log(obj1.regularMethod()); // 15
console.log(obj1.arrowMethod()); // NaN

⏱️ Preserving this in Async Callbacks

In asynchronous functions like setTimeout, regular functions lose their this binding. Arrow functions capture this from their lexical scope, making them ideal for preserving context in async callbacks.

const obj2 = {
  value: 50,
  method: function () {
    setTimeout(function () {
      console.log(this.value);
    }, 100);
  },
  methodArrow: function () {
    setTimeout(() => {
      console.log(this.value);
    }, 100);
  },
};
obj2.method(); // undefined
obj2.methodArrow(); // 50

🎯 Arrow Functions Preserve Context

Arrow functions retain the this value from where they were defined. Even when the method is detached from the object, it still correctly references this.value from its original context.

function Example() {
  this.value = 30;
  this.arrowMethod = () => {
    console.log(this.value);
  };
}
 
const example1 = new Example();
example1.arrowMethod(); // 30
 
const detachedMethod = example1.arrowMethod;
detachedMethod(); // 30

⚠️ Regular Functions Lose this When Detached

Regular functions bind this dynamically at call time. When a method is called standalone (detached from its object), this becomes undefined (in strict mode), leading to unexpected behavior.

function Example() {
  this.value = 30;
  this.regularMethod = function () {
    console.log(this);
  };
}
 
const example1 = new Example();
example1.regularMethod();
// Example { value: 30, regularMethod: [Function (anonymous)] }
 
const detachedMethod = example1.regularMethod;
detachedMethod(); // undefined

🧭 call with Regular vs Arrow Functions

Regular functions can have their this explicitly set using .call(). Arrow functions, however, do not bind this and always inherit it from the surrounding scope — making .call() ineffective.

const obj5 = {
  value: 25,
  regularMethod: function () {
    return this.value;
  },
  arrowMethod: () => {
    return this;
  },
};
 
const anotherObj = {
  value: 50,
};
 
console.log(obj5.regularMethod()); // 25
console.log(obj5.regularMethod.call(anotherObj)); // 50
console.log(obj5.arrowMethod()); // undefined
console.log(obj5.arrowMethod.call(anotherObj)); // undefined

🔄 Arrow Function Captures Outer this

When an arrow function is returned from a method, it keeps the this of its surrounding context — in this case, the obj7 object. This allows the inner function to access obj7.value even when returned from another object.

const obj = {
  value: 70,
  method: function () {
    return {
      getValue: () => {
        return this.value;
      },
    };
  },
};
 
const innerObj = obj.method();
console.log(innerObj.getValue()); // 70

🧩 this in Class Methods: Regular vs Arrow

In class instances, regular methods lose this when detached, while arrow functions preserve the class context. Arrow methods defined as properties are especially useful when passing methods as callbacks or event handlers.

class MyClass {
  value;
 
  constructor() {
    this.value = 40;
  }
 
  regularMethod() {
    console.log(this);
  }
 
  arrowMethod = () => {
    console.log(this);
  };
}
 
const instance = new MyClass();
 
instance.regularMethod();
// MyClass { value: 40, arrowMethod: [Function: arrowMethod] }
instance.arrowMethod();
// MyClass { value: 40, arrowMethod: [Function: arrowMethod] }
 
const regularFn = instance.regularMethod;
const arrowFn = instance.arrowMethod;
 
regularFn(); // undefined
arrowFn();
// MyClass { value: 40, arrowMethod: [Function: arrowMethod] }

🚗 Inheriting Methods with Object.create

Using Object.create(), you can create an object that inherits from another. Here, myCar inherits the getInfo method from vehicle, demonstrating how prototype-based inheritance works in JavaScript.

const vehicle = {
  getInfo: function () {
    console.log(this.model + " was made in " + this.year);
  },
};
 
const myCar = Object.create(vehicle);
myCar.model = "BMW";
myCar.year = 2010;
myCar.getInfo(); // BMW was made in 2010
console.log(myCar);
// { model: 'BMW', year: 2010 }
console.log(myCar.__proto__);
// { getInfo: [Function: getInfo] }

Output Every Second

setTimeout and setInterval

This example demonstrates two ways to print numbers at 1-second intervals using setInterval and recursive setTimeout. Both approaches achieve the same result, but setTimeout gives finer control over the delay between executions — especially useful for dynamic scheduling or error handling.

setInterval

function printNumbers(from, to) {
  let current = from;
  let timerId;
 
  function go() {
    console.log(current);
    if (current === to) {
      clearInterval(timerId);
    }
    current++;
  }
 
  go();
  timerId = setInterval(go, 1000);
}
 
printNumbers(5, 10);
// 5 immediately, and 6 to 10 with 1s between

setTimeout

function printNumbers(from, to) {
  let current = from;
 
  function go() {
    console.log(current);
    if (current < to) {
      setTimeout(go, 1000);
    }
    current++;
  }
 
  go();
}
 
printNumbers(5, 10);
// 5 immediately, and 6 to 10 with 1s between

Promise Chaining

promise-chaining

This example demonstrates how to chain Promises to perform sequential asynchronous operations. Each .then() receives the result of the previous one, allowing for clean, readable logic with predictable timing — in this case, doubling a number step by step with 1-second delays.

new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000);
})
  .then(function (result) {
    console.log(result); // 1
 
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(result * 2), 1000);
    });
  })
  .then(function (result) {
    console.log(result); // 2
 
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(result * 2), 1000);
    });
  })
  .then(function (result) {
    console.log(result); // 4
  });
// 1, 2, 4 with 1s between

Promise Error Handling

These examples show how to handle errors in promise chains using .catch(). They demonstrate how synchronous errors thrown during the executor function are automatically caught, while asynchronous errors (e.g., in setTimeout) are not — unless they're wrapped in a try...catch or returned from a rejected promise.

Understanding where and when errors can be caught is key to building reliable async flows.

new Promise((resolve, reject) => {
  throw new Error("Whoops!");
})
  .catch(function (error) {
    if (error instanceof URIError) {
      // handle it
    } else {
      console.log("Can't handle such error");
 
      throw error;
      // throwing this or another error jumps to the next catch
    }
  })
  .then(function () {
    /* doesn't run here */
  })
  .catch((error) => {
    console.error(`The unknown error has occurred: ${error.message}`);
    // don't return anything => execution goes the normal way
  });
 
// Can't handle such error
// The unknown error has occurred: Whoops!

⚠️ Synchronous vs Asynchronous Errors in Promises

Promises automatically catch synchronous errors thrown inside the executor. However, errors thrown asynchronously (e.g., inside setTimeout) are not caught unless explicitly wrapped in a rejecting Promise or a try...catch block inside async functions.

new Promise(function (resolve, reject) {
  throw new Error("Whoops!");
}).catch((e) => console.error(e.message)); // Whoops!
 
// There’s an “implicit try..catch” around the function code.
// So all synchronous errors are handled.
// But here the error is generated not while the executor is
// running, but later. So the promise can’t handle it.
 
new Promise(function (resolve, reject) {
  setTimeout(() => {
    throw new Error("Whoops!");
  }, 1000);
}).catch(console.error); // unhandled error ...

Proxy

This example uses a Proxy to intercept property access on an object. When a key is found in the dictionary, the translation is returned; otherwise, the original key is returned as a fallback. Proxies are powerful for defining custom behavior for fundamental operations like get, set, and more.

let dictionary = {
  Hello: "Hola",
  Bye: "Adiós",
};
 
dictionary = new Proxy(dictionary, {
  get(target, phrase) {
    // intercept reading a property from dictionary
    if (phrase in target) {
      // if we have it in the dictionary
      return target[phrase]; // return the translation
    } else {
      // otherwise, return the non-translated phrase
      return phrase;
    }
  },
});
 
// Look up arbitrary phrases in the dictionary!
// At worst, they're not translated.
console.log(dictionary["Hello"]); // Hola
console.log(dictionary["Welcome to Proxy"]); // Welcome to Proxy

Range Iterator

This example demonstrates how to create a custom iterator using the iterator protocol. The makeRangeIterator function generates numbers from start to end with a defined step, manually implementing the next() method. It’s useful for creating lazy sequences or custom iteration logic without relying on generators.

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let nextIndex = start;
  let iterationCount = 0;
 
  return {
    next() {
      let result;
 
      if (nextIndex < end) {
        result = { value: nextIndex, done: false };
        nextIndex += step;
        iterationCount++;
 
        return result;
      }
 
      return { value: iterationCount, done: true };
    },
  };
}
 
const iter = makeRangeIterator(1, 6, 2);
 
let result = iter.next();
while (!result.done) {
  console.log(result.value); // 1 3 5
  result = iter.next();
}
 
console.log("Iterated over sequence of size:", result.value);
// 1, 3, 5
// Iterated over sequence of size: 3

shooters

Every function is meant to output its number. All shooter functions are created in the lexical environment of makeArmy() function. But when army[5]() is called, makeArmy has already finished its job, and the final value of i is 10 (while stops at i=10). As the result, all shooter functions get the same value from the outer lexical environment and that is, the last value, i=10. Solution is to save variable let j = i

function makeArmy() {
  const shooters = [];
 
  let i = 0;
  while (i < 10) {
    let j = i; // save local variable
    const shooter = function () {
      // create a shooter function,
      return j; // that should show its number
    };
    shooters.push(shooter); // and add it to the array
    i++;
  }
 
  // ...and return the array of shooters
  return shooters;
}
 
const army = makeArmy();
 
console.log(army[0]()); // 0
console.log(army[1]()); // 1
console.log(army[5]()); // 5

this

Below users.customFilterNoThis(army.canJoin), throws, army.canJoin was called as a standalone function, with this=undefined, thus leading to an instant error. A call to users.customFilter(army.canJoin, army) can be replaced with users.customFilterNoThis(user => army.canJoin(user)), that does the same. This is an object before dot

Array.prototype.customFilter = function (callback, thisArg) {
  let result = [];
 
  for (let i = 0; i < this.length; i++) {
    if (callback.call(thisArg, this[i], i, this)) {
      result.push(this[i]);
    }
  }
 
  return result;
};
 
Array.prototype.customFilterNoThis = function (callback) {
  let result = [];
 
  for (let i = 0; i < this.length; i++) {
    if (callback(this[i], i, this)) {
      result.push(this[i]);
    }
  }
 
  return result;
};
 
const army = {
  minAge: 18,
  maxAge: 27,
  canJoin(user) {
    return user.age >= this.minAge && user.age < this.maxAge;
  },
};
 
const users = [{ age: 16 }, { age: 20 }, { age: 23 }, { age: 30 }];
 
const soldiers1 = users.customFilterNoThis(army.canJoin);
// return user.age >= this.minAge && user.age < this.maxAge;
// TypeError: Cannot read properties of undefined (reading 'minAge')
const soldiers2 = users.customFilterNoThis((user) => army.canJoin(user));
const soldiers3 = users.customFilter(army.canJoin, army);
 
console.log(soldiers2); // [ { age: 20 }, { age: 23 } ]
console.log(soldiers3); // [ { age: 20 }, { age: 23 } ]

throttling and debounce decorator

call-apply-decorators

Compared to the debounce decorator, throttling is completely different:

debounce

function debounce(func, ms) {
  let timeout;
 
  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), ms);
  };
}
 
const timeLoggedConsoleLog = (...args) => {
  console.log(`Logged after ${Date.now() - startTime} ms:`, ...args);
};
 
const startTime = Date.now();
const f = debounce(timeLoggedConsoleLog, 500);
 
f("a");
setTimeout(() => f("b"), 200);
setTimeout(() => f("c"), 600);
setTimeout(() => f("d"), 600);
setTimeout(() => f("e"), 600); // Logged after 1118 ms: e

throttling

function throttle(fn, limit) {
  let inThrottle;
 
  return function (...args) {
    if (inThrottle) return;
    fn.apply(this, args);
    inThrottle = true;
    setTimeout(() => (inThrottle = false), limit);
  };
}
 
const timeLoggedConsoleLog = (...args) => {
  console.log(`Logged after ${Date.now() - startTime} ms:`, ...args);
};
 
const startTime = Date.now();
const f = throttle(timeLoggedConsoleLog, 500);
 
f("a"); // Logged after 0 ms: a
setTimeout(() => f("b"), 200);
setTimeout(() => f("c"), 600); // Logged after 613 ms: c
setTimeout(() => f("d"), 600);
setTimeout(() => f("e"), 600);