Errors, regex, builtins

Abrupt completion, resource management, regular expressions, primitive wrappers, numbers, dates, buffers, Proxy, and Reflect.

The idea

File 06 left one thread hanging: errors that travel through a promise rather than up the call stack. This file picks up the other end — what “up the call stack” actually means — and then finishes the census with the standard library.

The organising idea for the first half is abrupt completion. Until now every statement has finished normally, handing control to the next line. A throw, a return, a break, and a continue all finish abruptly: they carry a value and a destination, and they abandon whatever was in progress. try/finally is the construct that lets two abrupt completions compete, and the rules for who wins are the whole story.

The second half is the standard library, and it is not filler. Regular expressions, numbers, and strings are where a construct you thought you understood behaves differently because the value has rules of its own — a regex that remembers where it stopped, a number that is secretly a float, a string that counts storage rather than characters.

try/catch/finally is about which outcome wins

A block produces an outcome: a value, a return, or a throw. finally gets the last word on it.

Then the rule that explains every surprising finally: a return in the finally block replaces whatever the try was going to do, including swallowing an exception outright. Most linters ban it for exactly that reason. One rule, and file 30 spends a page on its consequences.

The error hierarchy

Throwing an Error rather than a bare value is what gets you a stack trace. The builtin subclasses tell you who threw: a TypeError or RangeError came from the language itself, and your own subclass came from your code. That distinction is the reason to subclass at all.

using is finally made declarative

Declare a resource with using and its cleanup runs when the block ends — normally or by throwing — in reverse order of acquisition, with no pyramid of nested try blocks.

It is another protocol in file 05’s sense: implement [Symbol.dispose]() and using knows how to clean you up, or [Symbol.asyncDispose]() for await using. DisposableStack covers the case where the resources are not known until run time — add to it in a loop, dispose the lot at once.

FinalizationRegistry is the one that looks similar and is not. It fires after garbage collection: eventually, maybe, in no guaranteed order. It is a diagnostic tool, and nothing you actually need should depend on it.

A regex is a stateful object

Two things to carry through the regex section. First, a regex is an object, and with the g flag it is a stateful one — it remembers where it stopped in lastIndex. Second, the flags change the language the pattern is written in, not merely how it is applied.

The state is the trap. With /g, exec and test resume from lastIndex, so calling .test() twice on the same string alternates true and false. Never share a /g regex between calls. matchAll sidesteps the whole problem by cloning the regex, leaving your original untouched.

The flags worth knowing by name: g global, i ignoreCase, m multiline (^ and $ match at line breaks), s dotAll (. matches a newline), u unicode, y sticky (must match at lastIndex rather than searching forward — what you want in a tokenizer), v unicodeSets (set arithmetic inside […]), and d hasIndices (records the index range of every group).

The pattern features divide into groups that capture ((…), or named (?<year>…)), groups that do not ((?:…)), backreferences (\1, matching what a group already matched), and lookaround(?=…), (?!…), (?<=…), (?<!…) — which test whether something is there without consuming it.

new RegExp(string) builds a pattern at run time, and anything you interpolate into it needs escaping first. It is a real injection surface, not a stylistic concern.

Strings, numbers, and what the values themselves impose

Strings are immutable; every method returns a new one. The subtlety is what counts as one character: .length counts UTF-16 code units, and an emoji takes two of them. The iterator from file 05 walks code points instead, which is why iterating and .length disagree. normalize matters for the same reason from the other direction — the same visible character can have two encodings.

Watch for pairs of methods with the same shape and different answers. slice counts negatives from the end; substring clamps them to zero. Prefer slice.

Every number is a 64-bit float, including the ones that look like integers. That single fact produces 0.1 + 0.2 !== 0.3, Number.MAX_SAFE_INTEGER, and the existence of Number.EPSILON for comparisons. The global isNaN and isFinite coerce their argument; the Number.* versions do not — which is why isNaN("x") is true and Number.isNaN("x") is false.

BigInt is the answer to that limit: whole numbers of any size. It refuses to mix with Number in arithmetic, because no conversion is lossless in both directions — precision one way, fractions the other. Division truncates.

A Date is a millisecond count with an awkward API bolted on; months are 0-indexed and days are not. Read it, do not study it. Temporal is the eventual replacement.

Proxy and Reflect

A Proxy intercepts the fundamental operations on an object — reads, writes, key listing, deletion — by supplying traps named after them. Reflect provides the default behaviour of each one as an ordinary function, so a trap can do its work and then hand off.

The detail worth carrying: Reflect.get(target, prop, receiver) takes a receiver, and passing it through is what keeps this correct for an inherited getter behind the proxy. Forget it and inherited accessors silently break — file 02’s this rules and file 03’s chain meeting in one argument.

Where this leaves you

That completes the runtime census. Files 01–07 are one language: values, where they live, how they are produced, when they run, and how they fail. Everything after this point is a second language layered on top of it.

The shift is worth stating plainly before file 08 starts. Up to here, every claim in this course was checkable by running code — which is why every answer in passes II and III ends with a command. From file 08 onwards you are reading a system that is gone before any of it runs, whose claims are settled by the checker instead. Same source text, a completely different question asked of it.

While you read

The file

Read one construct at a time. When you meet a finally, work out the winning outcome before reading on. Mark anything you cannot explain with // ? in the source.

// 07 — ERRORS, REGEX, AND THE STANDARD BUILTINS
//
// A reading file. Read it straight down: the comment above each construct gives
// its NAME, and `// =>` gives what the expression on its left evaluates to, so
// nothing is left hanging. Each section uses only what the lines just above it
// introduced.
//
// It still runs (`node 07-errors-regex-builtins.ts`) and prints nothing; the
// `// =>` annotations are the output, already collected.
//
// Mark anything you can't explain out loud with  // ?

// ─── 1. THROW AND CATCH ──────────────────────────────────────────────────────
//
// The thing to hold on to about try/catch/finally is that it is about WHICH
// OUTCOME WINS. The block produces an outcome — a value, a return, a throw — and
// `finally` gets the last word on it.

const order: string[] = [];
let caught = "";
try {
  order.push("try");
  throw new Error("anything can be thrown, not just Errors");
} catch (e) {
  // In TypeScript `e` is `unknown` under useUnknownInCatchVariables, so it has to
  // be narrowed before you can touch it.
  if (e instanceof Error) caught = `${e.name}: ${e.message}`;
  order.push("catch");
} finally {
  order.push("finally");
}
const caughtMessage = caught; // => "Error: anything can be thrown, not just Errors"
const blockOrder = order; // => ["try", "catch", "finally"]

let boundless = "";
try {
  throw "a string"; // any value at all can be thrown
} catch { // OPTIONAL CATCH BINDING — no parameter needed if you don't inspect it
  boundless = "caught without binding";
}
const withoutBinding = boundless; // => "caught without binding"

// And here is that last word. A `return` in a finally block replaces whatever the
// try was going to do — including SWALLOWING an exception entirely. That is why
// linters ban it, and it is the one rule behind every surprising finally (30.1).
function finallyWins() {
  try {
    return "from try";
  } finally {
    // eslint-disable-next-line no-unsafe-finally
    return "from finally"; // beats the try's return, and would beat a throw too
  }
}
const whoWon = finallyWins(); // => "from finally"

function finallySwallows() {
  try {
    throw new Error("never seen by anyone");
  } finally {
    // eslint-disable-next-line no-unsafe-finally
    return "the throw is gone";
  }
}
const swallowed = finallySwallows(); // => "the throw is gone" — no error escapes at all

// ─── 2. THE ERROR HIERARCHY ──────────────────────────────────────────────────
//
// You can throw any value, but throwing an Error is what gets you a stack trace.
// The built-in subclasses tell you who threw: a TypeError came from the language,
// your own subclass came from your code.

const builtinErrors = [TypeError, RangeError, SyntaxError, ReferenceError, EvalError, URIError]
  .map((E) => E.name);
// => ["TypeError", "RangeError", "SyntaxError", "ReferenceError", "EvalError", "URIError"]

// `cause` keeps the original error when you wrap and rethrow — otherwise the
// interesting failure disappears and you are left holding your own message. It is
// defined non-enumerable, so it stays out of Object.keys and JSON (17.6).
const low = new Error("connection refused");
const high = new Error("could not load user", { cause: low });
const causeKept = high.cause === low; // => true
const causeIsHidden = Object.keys(high); // => [] — `message` and `cause` are both
// non-enumerable, which is why logging an error as an object shows you nothing

const aggregate = new AggregateError([low, high], "several failed");
const howMany = aggregate.errors.length; // => 2
const aggregateMessage = aggregate.message; // => "several failed"

// Two things to remember when subclassing. `name` is inherited and does NOT
// become your class name automatically, and older compile targets break the
// prototype chain, so `instanceof` needs an explicit repair there.
class ValidationError extends Error {
  override name = "ValidationError";
  readonly field: string;
  constructor(message: string, field: string) {
    super(message);
    this.field = field;
    Error.captureStackTrace?.(this, ValidationError); // V8-only; trims the trace so it
  } // starts at the caller instead of inside this constructor
}
const ve = new ValidationError("required", "email");
const isOwnClass = ve instanceof ValidationError; // => true
const isAlsoError = ve instanceof Error; // => true
const extraField = ve.field; // => "email"
const stringified = `${ve}`; // => "ValidationError: required" — name, colon, message
const defaultName = new (class extends Error {})("x").name; // => "Error" — a subclass that
// doesn't set `name` inherits it, which is the trap the line above avoids

// Error.isError distinguishes real errors even across realms (ES2025), which
// `instanceof` cannot do.
const realError = Error.isError?.(ve); // => true
const fakeError = Error.isError?.({ name: "Error", message: "fake" }); // => false

// ─── 3. EXPLICIT RESOURCE MANAGEMENT (`using`) ───────────────────────────────
//
// `using` is try/finally made declarative. Declare a resource and its cleanup
// runs when the block ends — normally or by throwing — in REVERSE order of
// acquisition, with no pyramid of nesting.

const lifecycle: string[] = [];

class Resource {
  id: string;
  constructor(id: string) {
    this.id = id;
    lifecycle.push(`acquired ${id}`);
  }
  [Symbol.dispose]() { // the hook. Implement it and `using` knows how to clean you up.
    lifecycle.push(`disposed ${this.id}`);
  }
}

{
  using first = new Resource("first");
  using second = new Resource("second");
  lifecycle.push(`body sees ${first.id} and ${second.id}`);
} // both disposed here, at the closing brace
const disposalOrder = lifecycle;
// => ["acquired first", "acquired second", "body sees first and second",
//     "disposed second", "disposed first"]
// Reverse order, so a resource is never torn down before something that might
// still be using it.

const asyncLifecycle: string[] = [];
class AsyncResource {
  async [Symbol.asyncDispose]() {
    asyncLifecycle.push("async disposed");
  }
}
{
  await using ar = new AsyncResource(); // `await using` awaits the disposal too
  void ar;
}
const afterAsyncBlock = asyncLifecycle; // => ["async disposed"]

// DisposableStack is for resources not known until run time: add to it in a loop
// and dispose the lot at once.
const stackLog: string[] = [];
{
  using stack = new DisposableStack();
  stack.defer(() => stackLog.push("deferred cleanup"));
  stack.use({ [Symbol.dispose]: () => stackLog.push("used resource disposed") });
}
const stackOrder = stackLog; // => ["used resource disposed", "deferred cleanup"]

// FinalizationRegistry fires after garbage collection — eventually, maybe, in
// some order. It is a diagnostic tool. Never put anything you need in it.
const registry = new FinalizationRegistry((held: string) => void held);
registry.register({}, "token");
const canUnregister = typeof registry.unregister; // => "function"

// ─── 4. REGULAR EXPRESSIONS ──────────────────────────────────────────────────
//
// Two things to carry through this section. A regex is an OBJECT, and with the /g
// flag it is a STATEFUL one that remembers where it got to. And the flags change
// the language the pattern is written in, not merely how it is applied.

const literalRe = /ab+c/gi; // LITERAL FORM — flags go after the closing slash
const constructedRe = new RegExp("ab+c", "gi"); // built from a string at run time, so
// anything interpolated needs escaping first. This is a real injection surface.
const patternText = literalRe.source; // => "ab+c" — the pattern without the slashes
const flagText = literalRe.flags; // => "gi" — always in a canonical order
const sameFlags = constructedRe.global; // => true

// flags: g global · i ignoreCase · m multiline · s dotAll · u unicode
//        y sticky · v unicodeSets · d hasIndices
const dotMatchesNewline = /./s.test("\n"); // => true — without /s, `.` never matches one
const multilineAnchor = /^b/m.test("a\nb"); // => true — /m makes ^ and $ match at breaks
const unicodeProperty = /\p{Letter}/u.test("é"); // => true — \p{...} needs /u or /v

const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; // NAMED CAPTURE GROUPS
const match = "2026-08-02".match(dateRe);
const whole = match?.[0]; // => "2026-08-02" — index 0 is always the entire match
const firstGroup = match?.[1]; // => "2026" — groups are numbered from 1
const byName = match?.groups?.year; // => "2026"
const matchIndex = match?.index; // => 0 — where the match started

const nonCapturing = /(?:ab)+/.test("abab"); // => true — (?:) groups without capturing
const backreference = /(\w)\1/.test("aa"); // => true — \1 means "the same text again"
const lookahead = /foo(?=bar)/.test("foobar"); // => true — (?=) checks without consuming
const negativeLookahead = /foo(?!bar)/.test("foobaz"); // => true
const lookbehind = /(?<=\$)\d+/.exec("$42")?.[0]; // => "42" — the $ is not in the result
const negativeLookbehind = /(?<!\$)\d+/.test("42"); // => true

// Here is the stateful part. With /g, `exec` and `test` resume from where the
// last call stopped, so calling test twice on the same string alternates true and
// false. Never share a /g regex between calls (33.3).
const stateful = /\d/g;
const firstHit = stateful.exec("1 2")?.index; // => 0
const cursorAfterFirst = stateful.lastIndex; // => 1 — the regex is now holding a position
const secondHit = stateful.exec("1 2")?.index; // => 2 — same string, different answer
const thirdHit = stateful.exec("1 2"); // => null — and this resets lastIndex to 0

// matchAll sidesteps the problem by cloning the regex, so your original is untouched.
const allLetters = [..."a1b2".matchAll(/(?<letter>[a-z])(\d)/g)].map((m) => m.groups?.letter);
// => ["a", "b"]

const replacedOnce = "a-b".replace(/-/, "_"); // => "a_b" — without /g, only the first
const replacedAll = "a-b-c".replaceAll("-", "_"); // => "a_b_c"
const byGroupName = "2026-08-02".replace(dateRe, "$<day>/$<month>/$<year>"); // => "02/08/2026"
const byFunction = "abc".replace(/b/, (m, offset) => `[${m}@${offset}]`); // => "a[b@1]c"
const splitByRe = "a1b2".split(/\d/); // => ["a", "b", ""] — a trailing empty piece, because
// the string ended with a separator
const searched = "a1b2".search(/\d/); // => 1 — the index, or -1

// The v flag (unicodeSets) adds set arithmetic inside [].
const setSubtraction = /[\p{Letter}--[aeiou]]/v.test("b"); // => true
const subtractedOut = /[\p{Letter}--[aeiou]]/v.test("a"); // => false
// The d flag records the index range of every capture group.
const indices = /(?<g>b)/d.exec("abc")?.indices?.groups?.["g"]; // => [1, 2]

// STICKY (/y) must match exactly AT lastIndex rather than searching forward from
// it — which is what you want when writing a tokenizer.
const sticky = /a/y;
sticky.lastIndex = 1;
const stuckMatch = sticky.test("ba"); // => true — "a" really is at index 1
const stuckCursor = sticky.lastIndex; // => 2
sticky.lastIndex = 0;
const stuckMiss = sticky.test("ba"); // => false — "a" exists, but not AT index 0

// ─── 5. STRINGS ──────────────────────────────────────────────────────────────
//
// Strings are immutable, and every method returns a new one. The subtlety to
// watch for is what counts as "one character": .length counts storage slots, and
// an emoji takes two of them.

const s = "  Hello, World  ";
const trimmed = s.trim(); // => "Hello, World"
const trimmedStart = s.trimStart(); // => "Hello, World  "
const trimmedEnd = s.trimEnd(); // => "  Hello, World"
const length = s.length; // => 16 — including the four spaces
const upper = s.toUpperCase(); // => "  HELLO, WORLD  "
const includes = s.includes("World"); // => true
const thirdFromEnd = s.at(-3); // => "d" — negative indices count back from the end
const firstL = s.indexOf("l"); // => 4
const lastL = s.lastIndexOf("l"); // => 12
const sliced = s.slice(2, 7); // => "Hello"
const substringed = s.substring(2, 7); // => "Hello" — identical, until a negative appears
const sliceFromEnd = "abc".slice(-2); // => "bc" — counts from the end
const substringClamps = "abc".substring(-2); // => "abc" — clamps the negative to 0.
// Same shape, different answer. Prefer slice.
const padded = "ab".padStart(5, "*"); // => "***ab"
const paddedEnd = "ab".padEnd(5, "*"); // => "ab***"
const repeated = "ab".repeat(3); // => "ababab"
const split = "a,b".split(","); // => ["a", "b"]
const splitWithLimit = "a,b,c".split(",", 2); // => ["a", "b"] — the rest is discarded
const splitToChars = "abc".split(""); // => ["a", "b", "c"] — but see the emoji below
const concatenated = "a".concat("b"); // => "ab"
const compared = "a".localeCompare("b"); // => -1 — negative, zero or positive, not a boolean
const spread = [..."ab"]; // => ["a", "b"] — strings are iterable
const startsWith = "abc".startsWith("a"); // => true
const endsWith = "abc".endsWith("c"); // => true

const decomposed = "héllo".normalize("NFD").length; // => 6 — e + combining accent
const composed = "héllo".normalize("NFC").length; // => 5 — one precomposed character
// Two strings that look identical can differ in storage. Normalize before comparing.

const charCode = "abc".charCodeAt(0); // => 97 — one UTF-16 unit
const codePoint = "😀".codePointAt(0); // => 128512 — the whole character
const fromCode = String.fromCharCode(97); // => "a"
const emojiLength = "😀".length; // => 2 — one character, two storage slots
const emojiSplit = "😀".split(""); // => ["\ud83d", "\ude00"] — split("") cuts it in half,
const emojiSpread = [..."😀"]; // => ["😀"] — iteration does not

// ─── 6. NUMBERS AND MATH ─────────────────────────────────────────────────────
//
// Every number is a 64-bit float, including the ones that look like integers.
// That is where 0.1 + 0.2 comes from, and why MAX_SAFE_INTEGER exists at all.

const floatSum = 0.1 + 0.2; // => 0.30000000000000004
const notEqual = 0.1 + 0.2 === 0.3; // => false
const closeEnough = Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON; // => true — the actual test
const maxSafe = Number.MAX_SAFE_INTEGER; // => 9007199254740991 — 2^53 - 1
const minSafe = Number.MIN_SAFE_INTEGER; // => -9007199254740991
const pastTheLimit = Number.isSafeInteger(2 ** 53); // => false

const isInteger = Number.isInteger(1.0); // => true — there is no separate integer type
const strictFinite = Number.isFinite("1"); // => false — no coercion, so a string is never
// @ts-expect-error TS2345: the global isFinite coerces; the Number.* version does not
const looseFinite = isFinite("1"); // => true — the global coerces "1" to 1 first
const strictNaN = Number.isNaN("x"); // => false — "x" is a string, not NaN
// @ts-expect-error TS2345: same story — isNaN("x") coerces to NaN and says true
const looseNaN = isNaN("x"); // => true — which is why the Number.* versions exist

const parsedFloat = Number.parseFloat("1.5px"); // => 1.5 — stops at the first bad character
const parsedInt = Number.parseInt("ff", 16); // => 255 — with an explicit radix
const numberCast = Number("1.5px"); // => NaN — Number() demands the WHOLE string
const fixed = (1234.5678).toFixed(2); // => "1234.57" — a string, not a number
const binary = (1234).toString(2); // => "10011010010"
const exponential = (0.000001234).toExponential(2); // => "1.23e-6"
const precision = (1234.5678).toPrecision(6); // => "1234.57"
const localised = (1234567).toLocaleString("en-US"); // => "1,234,567"

const positiveInfinity = 1 / 0; // => Infinity — no exception, just a value
const negativeInfinity = -1 / 0; // => -Infinity
const notANumber = 0 / 0; // => NaN
const zeroesAreEqual = -0 === 0; // => true
const zeroesAreDistinct = Object.is(-0, 0); // => false — and the sign survives division:
const dividedByNegativeZero = 1 / -0; // => -Infinity

const truncated = Math.trunc(-1.5); // => -1 — toward zero
const floored = Math.floor(-1.5); // => -2 — toward -Infinity
const ceiled = Math.ceil(-1.5); // => -1
const roundedHalf = Math.round(-1.5); // => -1 — .5 always rounds toward +Infinity,
const roundedOtherHalf = Math.round(-2.5); // => -2 // which is not "round half away from zero"
const noArguments = Math.max(); // => -Infinity — the identity for max
const noArgumentsMin = Math.min(); // => Infinity
const signOf = Math.sign(-3); // => -1
const hypotenuse = Math.hypot(3, 4); // => 5
const cubeRoot = Math.cbrt(27); // => 3
const leadingZeros = Math.clz32(1); // => 31 — count of leading zero bits in a 32-bit int

// BigInt is the answer to that safe-integer limit: whole numbers of any size. It
// refuses to mix with Number in arithmetic, because no conversion is lossless —
// precision one way, fractions the other.
const bigA = 2n ** 64n; // => 18446744073709551616n
const bigPlus = bigA + 1n; // => 18446744073709551617n
const bigType = typeof bigA; // => "bigint"
const bigDivision = 5n / 2n; // => 2n — division TRUNCATES; there are no fractions here
// @ts-expect-error TS2365: comparing bigint with number is legal JS, rejected by TS
const looseEqual = 1n == 1; // => true — `==` converts across the two types
const strictEqual = BigInt(1) === 1n; // => true — same type, same value
const backToNumber = Number(1n) + 1; // => 2 — an explicit conversion is always allowed

// ─── 7. DATES ────────────────────────────────────────────────────────────────
//
// A Date is a millisecond count with an awkward API bolted on. Read it, do not
// study it — Temporal is the eventual replacement.

const date = new Date(0); // EPOCH MILLISECONDS — zero is 1970-01-01T00:00:00Z
const iso = date.toISOString(); // => "1970-01-01T00:00:00.000Z" — always UTC
const millis = date.getTime(); // => 0
const nowIsLater = Date.now() > 0; // => true
const parsedYear = new Date("2026-08-02T00:00:00Z").getUTCFullYear(); // => 2026
const utcMonth = date.getUTCMonth(); // => 0 — MONTHS ARE 0-INDEXED. Days are not. Really.
const utcDay = date.getUTCDate(); // => 1 — see?
// `date.getMonth()` is the local-time version, so its answer depends on the
// machine's timezone: west of UTC this same instant is still December 1969.
const formatted = new Intl.DateTimeFormat("en-US", {
  dateStyle: "medium",
  timeZone: "UTC", // without this, the output depends on where you are sitting
}).format(date); // => "Jan 1, 1970"

// ─── 8. GLOBAL FUNCTIONS AND MISC ────────────────────────────────────────────

const encodedComponent = encodeURIComponent("a b&c"); // => "a%20b%26c" — escapes & and =
const encodedUri = encodeURI("a b/c"); // => "a%20b/c" — leaves URL structure alone
const decoded = decodeURIComponent("a%20b"); // => "a b"

const globalKind = typeof globalThis; // => "object"
const cloneKind = typeof structuredClone; // => "function"
const microtaskKind = typeof queueMicrotask; // => "function"

const randomBytes = crypto.getRandomValues(new Uint8Array(4)).length; // => 4 — the values
const uuidKind = typeof crypto.randomUUID(); // => "string" // are different every run
const uuidLength = crypto.randomUUID().length; // => 36

// TYPED ARRAYS AND BUFFERS — an ArrayBuffer is raw bytes; a view interprets them.
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setInt32(0, 42); // DataView defaults to BIG-endian
const throughDataView = view.getInt32(0); // => 42 — read back the same way, same answer
const throughTypedArray = new Int32Array(buffer)[0]; // => 704643072 — a typed array uses
// the PLATFORM's endianness, which on x86 is little-endian. Same four bytes, two
// readings. This is why DataView takes an explicit endianness argument.
const byteLength = new Uint8Array(buffer).length; // => 8 — same buffer, one byte per slot
const floats = new Float64Array([1.5]); // => Float64Array(1) [ 1.5 ]
const fromArray = Int8Array.from([1, 2]); // => Int8Array(2) [ 1, 2 ]

// A PROXY intercepts the basic operations on an object — reads, writes, key
// listing. Reflect provides the default behaviour for each, so a trap can do its
// work and then hand off. Watch the `receiver` being passed through: forget it and
// every inherited getter behind the proxy silently breaks (24.5).
const trapLog: string[] = [];
const target = { real: 1 };
const proxied = new Proxy(target, {
  get(t, prop, receiver) {
    trapLog.push(`get ${String(prop)}`);
    return Reflect.get(t, prop, receiver);
  },
  has: (t, prop) => Reflect.has(t, prop),
  set: (t, prop, value, receiver) => Reflect.set(t, prop, value, receiver),
  deleteProperty: (t, prop) => Reflect.deleteProperty(t, prop),
  ownKeys: (t) => Reflect.ownKeys(t),
});
const throughProxy = proxied.real; // => 1 — the value, with the trap in between
const inProxy = "real" in proxied; // => true — the `has` trap ran, not `get`
const keysOfProxy = Object.keys(proxied); // => ["real"] — `ownKeys`, plus a getOwnProperty-
// Descriptor call per key to decide which are enumerable
const whatWasTrapped = trapLog; // => ["get real"] — only the read went through `get`