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:
-
Promise Resolution Order Understand how
.then()
callbacks are queued as microtasks and run beforesetTimeout
. -
Microtasks vs. Macrotasks Compare how
Promise.then
,queueMicrotask
,setTimeout
, andrequestAnimationFrame
are scheduled and executed. -
Async/Await Behavior Explore how
await
impacts execution order relative to synchronous and timed code. -
Blocking the Event Loop See how synchronous loops (like
while
) delay the execution ofsetTimeout
. -
Practical Examples Trace execution order across complex interleavings of Promises and timeouts to build a mental model of how JavaScript scheduling works.
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
.
// 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:
- Method binding issues when referencing
this
- The difference between arrow functions and regular methods
- Preserving
this
in callbacks - Returning
this
for method chaining - Context behavior in
class
methods and prototype chains
Understanding these patterns is essential for working with objects, classes, and asynchronous code in JavaScript.
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
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
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
Compared to the debounce decorator, throttling is completely different:
debounce
runs the function once after the "cooldown" period.throttle
runs it not more often than givenms
time
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);