GUIDE · JavaScript
Errors, generators & misc
At a glance
- ERRORS
- throw new Error(msg, { cause }); custom classes via extends Error; try / catch (e) / finally; catch binding optional; instanceof checks Built-ins: Error TypeError RangeError SyntaxError ReferenceError AggregateError
- ITER
- [Symbol.iterator]() returns { next() -> { value, done } }; generators (function*) build them for you; spread/for-of/destructure consume them
- SAY
- fail fasterror boundaryrethrow with causelazy evaluationiterator protocolgenerator yieldswell-known symbols
The idea#
Think of a thrown error as a red tag on a part at a workshop bench. You stop the job, and the tag says what broke. throw new Error(message) is the ordinary tag. A custom class extends Error, so the tag can be a ValidationError with a field like 'age'. Pass { cause } into super and the new tag keeps the old one stapled on. Set this.name. Skip that and the name stays 'Error'. instanceof still walks the chain, so a ValidationError is an Error.
try is the attempt. catch reads the tag. The binding is optional when you do not need the object. finally sweeps the bench even after a return in try. The trap: a return inside finally wins, and the value you meant to hand back is replaced. You can throw a string. It works, and callers still expect an object with message and stack. Built-in tags: TypeError, ReferenceError, RangeError, SyntaxError, and AggregateError.
A generator is a parts drawer that hands you one piece when you pull, instead of dumping the drawer. That is lazy evaluation. The iterator protocol is the shape of the pull. [Symbol.iterator]() returns { next() }, and next() returns { value, done }. Spread, for...of, and destructuring pull until done. function* builds that object. yield is one piece. yield* lets another drawer finish the pull. After the first pull, next(arg) sends a value in.
A symbol is an unmarked bin. Symbol('id') is a key Object.keys will not list and JSON.stringify will drop. Two symbols with the same description are never equal. Symbol.for('a') uses a shop-wide registry, so the name returns the same symbol. Well-known symbols are hooks the language already looks for: Symbol.iterator for the pull, Symbol.toPrimitive for string versus number, Symbol.toStringTag for the [object ...] label.
Why it matters#
Fail fast means tag the bad part at the door. parseAge throws before 'x' or a negative age becomes a quiet NaN later. Rethrow with cause when a config load fails, so your message is on the outside and the parse error is still attached. An error boundary is the one place that catches and decides what happens next. A result object, { ok, value, error }, returns the tag instead of throwing. The sheet names that style and does not build it.
An endless drawer is useful because take can stop pulling. A date is a count of milliseconds since 1970-01-01 UTC, and Date.UTC numbers January as 0. Borrowing a tool from the next bench is CommonJS require or ESM import. A plain script needs neither.
Say fail fast, error boundary, rethrow with cause, lazy evaluation, iterator protocol, generator yields, and well-known symbols.
A small example#
class BadAge extends Error { constructor(message) { super(message); this.name = 'BadAge'; }} function parseAge(raw) { const n = Number(raw); if (!Number.isInteger(n) || n < 0) throw new BadAge('bad age'); return n;} function* range(lo, hi) { for (let i = lo; i < hi; i++) yield i;} const age = parseAge('12');const nums = [...range(0, 3)];age is 12. parseAge('x') throws a BadAge, which is also an Error. nums is [0, 1, 2].
What the code below is doing#
ValidationError takes a field, a message, and options, and passes the options to super so cause works. It sets name and field. NotFoundError sets name and a status of 404 with class fields. parseAge returns an integer or throws. A catch can match the class, or match name and field. The value is both a ValidationError and an Error, with a string stack. Bad JSON.parse is wrapped as new Error('config load failed', { cause }), and the cause is the SyntaxError.
finally runs even when try returns 'try'. An optional catch returns 'caught'. In h, try would return 1 and finally returns 2, so the result is 2. The built-in throws are null.x, a missing function, new Array(-1), and bad JSON. Throwing the string 'string' is the habit to drop.
countdown starts at 3. Spread produces [3, 2, 1]. Destructuring takes [3, 2]. Math.max pulls a fresh cursor and gets 3, because each call to Symbol.iterator starts over.
idGen yields forever. range(0, 10, 3) is [0, 3, 6, 9]. take(3, idGen(10)) is [10, 11, 12]. flatten uses yield* and turns nested arrays into [1, 2, 3, 4]. twoWay yields 'first', then next('hello') comes back as 'got hello'. An inorder walk yields [1, 2, 3]. Generators also have .return and .throw.
A symbol key stays out of Object.keys and JSON.stringify, and o[id] still reads 1. Separate Symbol('a') values never match. Symbol.for('a') does. Temp becomes '21°C' in a string and 22 in addition. Symbol.toStringTag reports [object Thing].
Date.UTC(2024, 0, 31, 12) is January, ISO 2024-01-31T12:00:00.000Z. Months are 0-based. getTime() is epoch milliseconds, and the epoch itself is 1970-01-01T00:00:00.000Z. February 2024 spans 29 days. getUTCDay() is 3, Sunday being 0. One UTC day is 86_400_000 ms. 'garbage' is an Invalid Date. Leave Date.now() out of the logic when the inputs are already timestamps.
The module notes are comments: CommonJS has __dirname, ESM has import.meta.url, and Node picks ESM from "type": "module" or a .mjs file.
The last block is the separator 1_000_000, a labeled continue outer / break outer, and checks that structuredClone, fetch, and queueMicrotask exist. The section title also names logical assignment. The lines that run do not use it.
Everything below is the demonstration. Same idea, written as code you can run.
'use strict';const assert = require('node:assert/strict');Custom errors#
class ValidationError extends Error { constructor(field, message, options) { super(message, options); // options.cause supported Node 16.9+ this.name = 'ValidationError'; // otherwise name is 'Error' this.field = field; }}class NotFoundError extends Error { name = 'NotFoundError'; status = 404; }function parseAge(s) { const n = Number(s); if (!Number.isInteger(n) || n < 0) throw new ValidationError('age', `bad age: ${s}`); return n;}try { parseAge('x'); } catch (e) {}try { try { JSON.parse('{'); } catch (e) { throw new Error('config load failed', { cause: e }); }} catch (e) {}Verified by 8 asserts
assert.equal(parseAge('42'), 42);assert.throws(() => parseAge('x'), ValidationError);assert.throws(() => parseAge('-1'), { name: 'ValidationError', field: 'age' }); // match by properties assert.equal(e instanceof ValidationError && e instanceof Error, true); assert.equal(e.message, 'bad age: x'); assert.equal(typeof e.stack, 'string'); assert.equal(e.cause instanceof SyntaxError, true); // wrap and keep the originalassert.equal(new NotFoundError('nope').status, 404);try / catch / finally semantics#
function f() { try { return 'try'; } finally { sideEffect = 'finally ran'; } // finally runs even after return}let sideEffect = '';function g() { try { throw new Error('x'); } catch { return 'caught'; } } // optional catch bindingfunction h() { try { return 1; } finally { return 2; } } // finally's return WINS (don't do this)// Result-style alternative: const [err, val] = await to(promise) — or return { ok, value, error }Verified by 9 asserts
assert.equal(f(), 'try');assert.equal(sideEffect, 'finally ran');assert.equal(g(), 'caught');assert.equal(h(), 2);assert.throws(() => { null.x; }, TypeError);assert.throws(() => { undefinedFn(); }, ReferenceError);assert.throws(() => new Array(-1), RangeError);assert.throws(() => JSON.parse('nope'), SyntaxError);assert.throws(() => { throw 'string'; }, /string/); // you CAN throw non-Errors; don'tIterator protocol by hand#
const countdown = { from: 3, [Symbol.iterator]() { let n = this.from; return { next: () => (n > 0 ? { value: n--, done: false } : { value: undefined, done: true }) }; },};const [c1, c2] = countdown;Verified by 3 asserts
assert.deepEqual([...countdown], [3, 2, 1]);assert.deepEqual([c1, c2], [3, 2]);assert.equal(Math.max(...countdown), 3);Generators#
function* idGen(start = 1) { let id = start; while (true) yield id++; }const ids = idGen();function* range(lo, hi, step = 1) { for (let i = lo; i < hi; i += step) yield i; }function* take(n, it) { for (const x of it) { if (n-- <= 0) return; yield x; } }function* flatten(xs) { for (const x of xs) Array.isArray(x) ? yield* flatten(x) : yield x; } // yield* delegatesfunction* twoWay() { const got = yield 'first'; yield `got ${got}`; }const tw = twoWay();tw.next();function* inorder(node) { if (!node) return; yield* inorder(node.l); yield node.v; yield* inorder(node.r); }Verified by 7 asserts
assert.deepEqual([ids.next().value, ids.next().value], [1, 2]);assert.deepEqual([...range(0, 10, 3)], [0, 3, 6, 9]);assert.deepEqual([...take(3, idGen(10))], [10, 11, 12]); // lazy: infinite source, finite consumptionassert.deepEqual([...flatten([1, [2, [3, [4]]]])], [1, 2, 3, 4]);assert.equal(tw.next('hello').value, 'got hello'); // next(arg) sends a value INTO the generatorassert.deepEqual([...inorder({ v: 2, l: { v: 1 }, r: { v: 3 } })], [1, 2, 3]);assert.equal(typeof idGen().return, 'function'); // .return() / .throw() for cleanup & injectionSymbols#
const id = Symbol('id');const o = { [id]: 1, visible: 2 };class Temp { constructor(c) { this.c = c; } [Symbol.toPrimitive](hint) { return hint === 'string' ? `${this.c}°C` : this.c; } }Verified by 8 asserts
assert.deepEqual(Object.keys(o), ['visible']); // symbol keys are not enumeratedassert.equal(o[id], 1);assert.equal(Symbol('a') === Symbol('a'), false);assert.equal(Symbol.for('a') === Symbol.for('a'), true); // global registryassert.equal(JSON.stringify(o), '{"visible":2}');assert.equal(`${new Temp(21)}`, '21°C');assert.equal(new Temp(21) + 1, 22);assert.equal(Object.prototype.toString.call({ [Symbol.toStringTag]: 'Thing' }), '[object Thing]');Dates#
enough for an ICA task that hands you timestamps
const d = new Date(Date.UTC(2024, 0, 31, 12, 0, 0)); // months are 0-based!const addDays = (date, n) => new Date(date.getTime() + n * 86_400_000);// In ICA-style tasks timestamps are usually plain ints passed in. Never call Date.now() inside the logic.Verified by 9 asserts
assert.equal(d.toISOString(), '2024-01-31T12:00:00.000Z');assert.equal(d.getTime(), 1706702400000); // ms since epochassert.equal(new Date('2024-01-31T12:00:00Z').getTime(), d.getTime());assert.equal(new Date(0).toISOString(), '1970-01-01T00:00:00.000Z');assert.equal(Math.floor((new Date('2024-03-01') - new Date('2024-02-01')) / 86_400_000), 29); // leap yearassert.equal(d.getUTCDay(), 3); // Wednesday (0 = Sunday)assert.equal(addDays(d, 1).toISOString().slice(0, 10), '2024-02-01');assert.equal(Number.isNaN(new Date('garbage').getTime()), true); // "Invalid Date"assert.equal(typeof Date.now(), 'number');Modules#
CommonJS vs ESM one-liners
// CJS: const x = require('./x'); module.exports = { a }; exports.b = ...; __dirname available// ESM: import x from './x.js'; export const a = 1; export default ...; import.meta.url// Node picks ESM if "type": "module" in package.json or .mjs extension. CodeSignal uses a plain script — no imports needed.Numeric separators / optional catch / logical assignment / labeled break (rare but legal)#
outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (j === 1) continue outer; if (i === 2) break outer; } }Verified by 4 asserts
assert.equal(1_000_000, 1000000);assert.equal(typeof structuredClone, 'function'); // Node 17+assert.equal(typeof globalThis.fetch, 'function'); // Node 18+assert.equal(typeof queueMicrotask, 'function');See also