Advanced JavaScript
Advanced Language Features
A Practical Guide
2026
Chapter 17: Symbols
Creating and using symbols
Why use symbols?
Well-known symbols
Global symbol registry
Best practices
Chapter 18: Iterators
The iterator protocol, manually
Making a custom object iterable
Built-in iterables
Best practices
Chapter 19: Generators
Basic generator syntax
yield pauses execution
Passing values into a generator
Delegating with yield*
Async generators
Best practices
Chapter 20: Strict Mode
Enabling strict mode
What strict mode changes
Best practices
Chapter 21: Error Handling and try/catch
Basic try/catch/finally
Catching specific error types
Custom error classes
Async error handling
Optional catch binding and error causes
Best practices
Part 4 Summary
Chapter 17: Symbols
Symbol is a primitive type introduced in ES6 that creates guaranteed-unique values, typically used as object property keys
that won’t collide with other keys — even ones with the same description.
Creating and using symbols
const id1 = Symbol("id");
const id2 = Symbol("id");
[Link](id1 === id2); // false - every symbol is unique, even with the same description
const user = {
name: "Alice",
[id1]: 12345
};
[Link](user[id1]); // 12345
[Link]([Link](user)); // ["name"] - symbol keys are hidden from normal enumeration
Why use symbols?
They let you attach “hidden” metadata to objects without risking collisions with existing or future string keys — useful in
libraries that shouldn’t clash with user-defined properties.
const LIBRARY_META = Symbol("libraryMeta");
function tag(obj, meta) {
obj[LIBRARY_META] = meta;
return obj;
}
const config = { debug: true };
tag(config, { version: "1.0" });
[Link](config); // { debug: true } - the symbol property doesn't show up in normal logs/loops
Well-known symbols
JavaScript itself uses symbols to let you customize built-in behavior. The most common is [Link] , covered in Chapter
18.
class Range {
constructor(start, end) {
[Link] = start;
[Link] = end;
}
[[Link]]() {
let current = [Link];
const end = [Link];
return {
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true };
}
};
}
}
[Link]([...new Range(1, 5)]); // [1, 2, 3, 4, 5]
Global symbol registry
[Link](key) creates (or reuses) a symbol shared across your entire application via a global registry — unlike Symbol() ,
which always creates a new unique value.
const a = [Link]("[Link]");
const b = [Link]("[Link]");
[Link](a === b); // true - same key returns the same symbol
Best practices
Use symbols for library-internal metadata or “protocol” properties (like custom iterators), not as a general privacy
mechanism — private class fields ( #field ) are better for true privacy.
Use [Link]() only when you deliberately need the same symbol across different files/modules.
Chapter 18: Iterators
An iterator is any object implementing the iterator protocol: a next() method that returns { value, done } . Iterables are
objects implementing [Link] , which makes them work with for...of , spread, and destructuring.
The iterator protocol, manually
function makeCounter(limit) {
let count = 0;
return {
next() {
if (count < limit) {
return { value: count++, done: false };
}
return { value: undefined, done: true };
}
};
}
const counter = makeCounter(3);
[Link]([Link]()); // { value: 0, done: false }
[Link]([Link]()); // { value: 1, done: false }
[Link]([Link]()); // { value: 2, done: false }
[Link]([Link]()); // { value: undefined, done: true }
Making a custom object iterable
Any object becomes iterable — and therefore usable in for...of , spread [...obj] , and destructuring — by implementing
[Link] .
class LinkedList {
constructor() {
[Link] = [];
}
add(item) {
[Link](item);
return this;
}
[[Link]]() {
let index = 0;
const items = [Link];
return {
next: () => index < [Link]
? { value: items[index++], done: false }
: { value: undefined, done: true }
};
}
}
const list = new LinkedList().add("a").add("b").add("c");
for (const item of list) {
[Link](item); // a, b, c
}
[Link]([...list]); // ['a', 'b', 'c']
Built-in iterables
Arrays, strings, Map , Set , and NodeList are all iterable by default — this is precisely why for...of works on all of them.
for (const char of "hi") [Link](char); // h, i
for (const [key, value] of new Map([["a", 1], ["b", 2]])) [Link](key, value);
Best practices
Implement [Link] on custom data structures (queues, trees, linked lists) so they integrate naturally with
for...of and spread.
Prefer for...of over manually calling .next() unless you need fine-grained control over iteration.
Chapter 19: Generators
A generator is a special function ( function* ) that can pause and resume execution, automatically implementing the iterator
protocol for you.
Basic generator syntax
function* countUpTo(max) {
for (let i = 1; i <= max; i++) {
yield i;
}
}
const gen = countUpTo(3);
[Link]([Link]()); // { value: 1, done: false }
[Link]([Link]()); // { value: 2, done: false }
[Link]([Link]()); // { value: 3, done: false }
[Link]([Link]()); // { value: undefined, done: true }
// Generators are iterable, so they work with for...of directly
for (const n of countUpTo(5)) [Link](n); // 1 2 3 4 5
yield pauses execution
Each call to .next() resumes the function until the next yield , making generators ideal for lazy sequences — including infinite
ones.
function* infiniteIds() {
let id = 1;
while (true) {
yield id++;
}
}
const ids = infiniteIds();
[Link]([Link]().value); // 1
[Link]([Link]().value); // 2
[Link]([Link]().value); // 3
// This never runs out of memory because values are produced on demand
Passing values into a generator
function* chatBot() {
const name = yield "What's your name?";
const mood = yield `Hi ${name}, how are you?`;
return `Nice talking to you, ${name} (${mood})`;
}
const bot = chatBot();
[Link]([Link]().value); // "What's your name?"
[Link]([Link]("Alice").value); // "Hi Alice, how are you?"
[Link]([Link]("great").value); // "Nice talking to you, Alice (great)"
Delegating with yield*
function* inner() {
yield 1;
yield 2;
}
function* outer() {
yield 0;
yield* inner();
yield 3;
}
[Link]([...outer()]); // [0, 1, 2, 3]
Async generators
Generators combine with async / await to lazily produce asynchronous values — useful for paginated API results.
async function* fetchPages(url) {
let nextUrl = url;
while (nextUrl) {
const res = await fetch(nextUrl);
const page = await [Link]();
yield [Link];
nextUrl = [Link];
}
}
for await (const items of fetchPages("/api/items")) {
[Link](items);
}
Best practices
Use generators for lazy evaluation: infinite sequences, large datasets processed in chunks, or custom iteration logic.
Use async generators for streaming/paginated data instead of loading everything into memory at once.
Chapter 20: Strict Mode
"use strict" opts your code into a restricted variant of JavaScript that catches common mistakes and disables some confusing
legacy behavior.
Enabling strict mode
"use strict"; // at the top of a file - applies to the whole file
function example() {
"use strict"; // or at the top of a single function
}
Note: ES6 modules and classes are always in strict mode automatically — you don’t need the directive in modern module-
based code.
What strict mode changes
Assigning to undeclared variables throws instead of creating a global:
"use strict";
x = 10; // ReferenceError: x is not defined
Silent failures become real errors:
"use strict";
const obj = [Link]({ name: "Alice" });
[Link] = "Bob"; // TypeError (non-strict mode fails silently instead)
Duplicate parameter names are disallowed:
"use strict";
function broken(a, a) {} // SyntaxError: Duplicate parameter name not allowed in this context
this is undefined in plain function calls, instead of the global object:
"use strict";
function whoAmI() {
[Link](this);
}
whoAmI(); // undefined (non-strict mode: the global/window object)
Octal literals and with statements are disallowed entirely — both are legacy sources of bugs.
Best practices
In modern code, you get strict mode for free via ES modules and classes — no need to add the directive manually in most
projects.
If writing plain <script> (non-module) code, add "use strict" at the top of every file to catch mistakes early.
Chapter 21: Error Handling and try/catch
Robust error handling is what separates prototype code from production code.
Basic try/catch/finally
try {
const data = [Link]("{ invalid json");
} catch (error) {
[Link]("Failed to parse:", [Link]);
} finally {
[Link]("This always runs, error or not");
}
Catching specific error types
try {
riskyOperation();
} catch (error) {
if (error instanceof TypeError) {
[Link]("A type error occurred:", [Link]);
} else if (error instanceof RangeError) {
[Link]("A range error occurred:", [Link]);
} else {
throw error; // re-throw anything we don't know how to handle
}
}
Custom error classes
class ValidationError extends Error {
constructor(message, field) {
super(message);
[Link] = "ValidationError";
[Link] = field;
}
}
function validateAge(age) {
if (age < 0) {
throw new ValidationError("Age cannot be negative", "age");
}
return age;
}
try {
validateAge(-5);
} catch (error) {
if (error instanceof ValidationError) {
[Link](`Validation failed on field "${[Link]}": ${[Link]}`);
}
}
Async error handling
async function loadData() {
try {
const response = await fetch("/api/data");
if (![Link]) throw new Error(`HTTP ${[Link]}`);
return await [Link]();
} catch (error) {
[Link]("Load failed:", [Link]);
return null; // graceful fallback
}
}
Optional catch binding and error causes
try {
doSomething();
} catch {
// ES2019: you don't have to name the error if you don't need it
[Link]("Something went wrong");
}
// ES2022: chain the underlying cause of an error
try {
parseConfig();
} catch (err) {
throw new Error("Failed to start application", { cause: err });
}
Best practices
Never use empty catch blocks that swallow errors silently — at minimum, log them.
Create custom error classes for domain-specific failures ( ValidationError , NotFoundError ) to make handling logic in catch
blocks precise.
Use { cause } to preserve the original error when wrapping/rethrowing, so debugging doesn’t lose context.
Part 4 Summary
Feature Purpose
Symbol Unique, collision-free property keys
Iterator protocol Makes custom objects work with for...of /spread
Generators ( function* ) Pausable functions, lazy sequences
Strict mode Catches silent errors and legacy footguns
try/catch/finally Structured error handling
Next: Part 5 — Browser Storage, covering localStorage and sessionStorage .