JavaScript (ES2026) Module 9 – Errors and debugging
Built-in error types and engine messages
TypeError, RangeError, ReferenceError, SyntaxError, URIError, AggregateError: what throws each, which to throw yourself, and checks that work in every engine.
What you will learn
- Recognise the operations that throw TypeError, RangeError, ReferenceError, SyntaxError and URIError
- Choose the built-in error class that a function of your own should throw
- Write checks and tests that rely on an error's class or code, never on an engine's message
- Identify Node.js errors by their code and web API errors by their DOMException name
Before you start
What you’ll learn · Before you start
What you will learn
- Recognise the operations that throw TypeError, RangeError, ReferenceError, SyntaxError and URIError
- Choose the built-in error class that a function of your own should throw
- Write checks and tests that rely on an error's class or code, never on an engine's message
- Identify Node.js errors by their code and web API errors by their DOMException name
Before you start
On this page
JavaScript reports a failure with an object of the Error family, and the object’s class says what kind of failure
it was. ECMA-262 defines the classes that engines throw: TypeError when an operation cannot work on the value it
was given, RangeError when a value is outside the allowed set or range, ReferenceError when a name cannot be
resolved, SyntaxError when text cannot be parsed, URIError when the URI functions are misused, and
AggregateError for several failures at once; EvalError remains only for compatibility. Hosts add their own: web
APIs throw a DOMException identified by its name, and Node.js puts a stable code on its errors. Your functions
should throw the same classes, chosen by the same rules, so that their callers can tell a bug from bad input. The
class is meant for code. The message is meant for people, and it changes between engines and between releases.
The family and what each class means
| Class | ECMA-262’s meaning | Your own code throws it when |
|---|---|---|
TypeError |
An operation failed and none of the other classes fits | An argument is the wrong kind of value: text for a number, null for an object, something that is not a function |
RangeError |
A value is not in the set or range of allowed values | A number is too big, too small or not whole, or a string is not one of the accepted choices |
ReferenceError |
An invalid reference was found | Never: it describes the program’s own names |
SyntaxError |
Text could not be parsed | Text in a format your code parses cannot be read |
URIError |
A global URI function was used against its definition | Rarely: it belongs to encodeURI and its relatives |
AggregateError |
Several errors are reported together | One check finds several problems, such as several invalid fields of a form |
EvalError |
No longer used by the specification | Never |
The classes share one design. Each has its own prototype, which inherits from Error.prototype, and the name
lives on that prototype rather than on each error. String(error) uses the inherited Error.prototype.toString,
which joins the name and the message with a colon. All of them can be called with or without new.
// What every built-in error shares: Error.prototype in its chain, a name on its own prototype, and toString().
const classes = [TypeError, RangeError, ReferenceError, SyntaxError, URIError, EvalError, AggregateError];
for (const C of classes) {
const error = C === AggregateError ? new C([], 'two checks failed') : new C('two checks failed');
console.log(
`${C.name.padEnd(14)} instanceof Error: ${error instanceof Error}, name from the prototype: ${!Object.hasOwn(error, 'name')}, ${String(error)}`,
);
}
const withoutNew = RangeError('no new needed');
console.log(withoutNew instanceof RangeError, withoutNew.message); Output
TypeError instanceof Error: true, name from the prototype: true, TypeError: two checks failed RangeError instanceof Error: true, name from the prototype: true, RangeError: two checks failed ReferenceError instanceof Error: true, name from the prototype: true, ReferenceError: two checks failed SyntaxError instanceof Error: true, name from the prototype: true, SyntaxError: two checks failed URIError instanceof Error: true, name from the prototype: true, URIError: two checks failed EvalError instanceof Error: true, name from the prototype: true, EvalError: two checks failed AggregateError instanceof Error: true, name from the prototype: true, AggregateError: two checks failed true no new needed
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node family.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
What makes the engine throw each one
Twenty operations that fail, sorted by the class the engine throws:
// Operations that fail, grouped by the error class the engine throws for them. Only the class is printed: the
// messages are each engine's own words.
const cases = [
['call a number as a function', () => (42)()],
['read a property of undefined', () => undefined.total],
['use new on an arrow function', () => new (() => {})()],
['call a class without new', () => { class Cart {} Cart(); }],
['add a BigInt to a number', () => 10n + 1],
['assign to a frozen property', () => { Object.freeze({ qty: 1 }).qty = 2; }],
['stringify a circular object', () => { const a = {}; a.self = a; JSON.stringify(a); }],
['loop over a number with for…of', () => { for (const x of 7) console.log(x); }],
['make an array of length -1', () => new Array(-1)],
['round to 101 digits with toFixed', () => (1.5).toFixed(101)],
['repeat a string -1 times', () => 'ab'.repeat(-1)],
['turn 1.5 into a BigInt', () => BigInt(1.5)],
['ask for an invalid date as ISO text', () => new Date(NaN).toISOString()],
['read a name that was never declared', () => totl],
['read a let before its line', () => { later; let later = 1; }],
['parse invalid JSON', () => JSON.parse('{"qty": 2,}')],
['build a regex from "["', () => new RegExp('[')],
['compile a function body with new Function', () => new Function('return {')],
['decode the text "%"', () => decodeURIComponent('%')],
['encode a lone surrogate', () => encodeURIComponent('\uD800')],
];
for (const [what, operation] of cases) {
try {
operation();
console.log(`${'no error'.padEnd(15)} ${what}`);
} catch (error) {
console.log(`${error.constructor.name.padEnd(15)} ${what}`);
}
} Output
TypeError call a number as a function TypeError read a property of undefined TypeError use new on an arrow function TypeError call a class without new TypeError add a BigInt to a number TypeError assign to a frozen property TypeError stringify a circular object TypeError loop over a number with for…of RangeError make an array of length -1 RangeError round to 101 digits with toFixed RangeError repeat a string -1 times RangeError turn 1.5 into a BigInt RangeError ask for an invalid date as ISO text ReferenceError read a name that was never declared ReferenceError read a let before its line SyntaxError parse invalid JSON SyntaxError build a regex from "[" SyntaxError compile a function body with new Function URIError decode the text "%" URIError encode a lone surrogate
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node error-zoo.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The output is the same in Node.js and in the Run button’s QuickJS engine, line for line, because the class of each error is fixed by the standard. A few of the lines deserve a closer look.
TypeErroris the widest class. ECMA-262 uses it for every failed operation that no other class describes, so it covers calling something that is not a function, reading a property ofundefinedornull, breaking the rules ofnewand classes, mixingBigIntwith ordinary numbers, and iterating a value that is not iterable.- Writing to a frozen property throws only in strict code. Modules are always strict, so the example threw. In a
classic script without
'use strict'the same assignment is skipped without a word, which hides more bugs than it prevents. RangeErrorguards lengths, counts and digits. The value had the right type, a number, but not an allowed one: a negative array length, 101 decimals, a repeat count of −1, a fraction whereBigIntneeds a whole number.SyntaxErrorcomes in two kinds. Text that the engine parses while your code runs, throughJSON.parse,new RegExpornew Function, throws aSyntaxErrorthat acatchcan handle. A syntax error in a script itself is found before any of the script runs, so nothing in that script can catch it.- Engine limits are the exception to the rule. When recursion goes too deep, V8, the engine of Chrome and
Node.js, throws a
RangeErrorwith the message “Maximum call stack size exceeded”, while Firefox throws anInternalError, a class of its own that no standard defines. Code that must react to a stack overflow everywhere cannot rely on one class.
Choosing the class your function throws
When your own function rejects an argument, throw the class that describes the mistake. The order of the questions matters: check the kind of value first, then whether the value is allowed, then the rules of your application.
Which error should your function throw?
Text description of the diagram
Ask the questions from the top; the first one answered "yes" decides.
1. Is the argument the wrong kind of value, such as text where a number belongs or `null` where an object belongs? Then throw a `TypeError`. 2. Is it the right kind of value but not one of the allowed ones, such as a quantity of 0 or an unknown status? Then throw a `RangeError`. 3. Is it text in a format your own code parses, and it cannot be parsed? Then throw a `SyntaxError`, or a parse error class of your own. 4. Is a rule of your application broken, such as a seat that is already taken or a balance that is too low? Then throw an error class of your own. 5. Anything else: throw an `Error` whose message names the value that caused it.
The difference between the first two questions is the one people blur most often. A TypeError says the caller
passed the wrong kind of thing, which usually means the calling code is wrong. A RangeError says the value was the
right kind but not acceptable, which often means a person made a choice that has to change. ECMA-262 defines
RangeError for values outside a “set or range” of allowed values, so an unknown status string is a RangeError as
much as a quantity of 0 is; Intl.NumberFormat, for one, throws it for a currency code that is not three letters.
// Two kinds of mistake, two classes: the wrong kind of value is a TypeError, a value outside the allowed ones is a
// RangeError. The caller treats them differently.
const MAX_PER_ORDER = 10;
function setQuantity(cart, sku, qty) {
if (typeof qty !== 'number') throw new TypeError(`qty must be a number, got ${typeof qty}`);
if (!Number.isInteger(qty) || qty < 1 || qty > MAX_PER_ORDER) {
throw new RangeError(`qty must be a whole number from 1 to ${MAX_PER_ORDER}, got ${qty}`);
}
cart.set(sku, qty);
}
const cart = new Map();
for (const qty of [2, 0, 2.5, 25, '3']) {
try {
setQuantity(cart, 'TEA-250', qty);
console.log(`${JSON.stringify(qty).padEnd(5)} -> cart has ${cart.get('TEA-250')}`);
} catch (error) {
if (error instanceof RangeError) {
console.log(`${JSON.stringify(qty).padEnd(5)} -> tell the shopper: ${error.message}`);
} else {
console.log(`${JSON.stringify(qty).padEnd(5)} -> a bug in the calling code: ${error.name}`);
}
}
} Output
2 -> cart has 2 0 -> tell the shopper: qty must be a whole number from 1 to 10, got 0 2.5 -> tell the shopper: qty must be a whole number from 1 to 10, got 2.5 25 -> tell the shopper: qty must be a whole number from 1 to 10, got 25 "3" -> a bug in the calling code: TypeError
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node choose-type.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The caller tells the shopper about every RangeError, because those messages describe a choice the shopper made. The
text '3' produced a TypeError instead: a quantity box returns text, and the code that called setQuantity
forgot to convert it, which no message to the shopper can fix. Two rules complete the picture. Never throw a
ReferenceError or an EvalError yourself, because they describe the program, not its data. And when a rule of your
application is broken, such as a seat that is already booked, give the failure an error class of its own, which the
next lesson builds; a built-in class would make it look like an ordinary programming mistake.
AggregateError: several failures in one throw
Some checks find more than one problem, and a form that reports its mistakes one at a time makes people submit it
again and again. An AggregateError carries a list of errors in its errors property, next to its own message:
// Report every problem of a form at once: one AggregateError that carries an error per field.
function validateSignup(form) {
const problems = [];
if (typeof form.name !== 'string' || form.name.trim() === '') problems.push(new TypeError('name is required'));
if (!/^\d{6}$/.test(form.pin ?? '')) problems.push(new RangeError('pin code must have 6 digits'));
if (!Number.isInteger(form.age) || form.age < 18) problems.push(new RangeError('age must be a whole number, 18 or more'));
if (problems.length > 0) throw new AggregateError(problems, `${problems.length} fields need attention`);
return { ...form, name: form.name.trim() };
}
try {
validateSignup({ name: ' ', pin: '5600', age: 17.5 });
} catch (error) {
console.log(`${error.name}: ${error.message}`);
for (const problem of error.errors) console.log(` ${problem.name}: ${problem.message}`);
}
console.log(validateSignup({ name: ' Asha ', pin: '560001', age: 34 }).name); Output
AggregateError: 3 fields need attention TypeError: name is required RangeError: pin code must have 6 digits RangeError: age must be a whole number, 18 or more Asha
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node aggregate-validation.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The standard library throws one in a single place today: Promise.any rejects with an AggregateError whose
errors hold every reason when all of its promises reject. A newer relative, SuppressedError, comes with explicit
resource management, the using declaration, which TC39 expects to publish in ECMAScript 2027. It carries the error
thrown while a resource was being released in error, and the error that was already on its way in suppressed.
Node.js 24 has both; the Run button’s engine has neither.
Messages are written for people
The message says what went wrong in words that the engine chose, and engines choose differently. This check runs the
same JSON.parse call twice, once asking for the class and once comparing the message with V8’s wording:
// Two checks of the same failure. The first asks for the class, which ECMA-262 fixes; the second compares the
// message with the words one engine happens to use.
function check(label, passed) {
console.log(`${passed ? 'pass' : 'FAIL'} ${label}`);
}
let error;
try {
JSON.parse('{"qty": 2,}');
} catch (caught) {
error = caught;
}
check('the error is a SyntaxError', error instanceof SyntaxError);
check('the message is the one V8 writes', error.message === 'Expected double-quoted property name in JSON at position 10 (line 1 column 11)');
console.log(`message here: ${error.message}`); Output
pass the error is a SyntaxError pass the message is the one V8 writes message here: Expected double-quoted property name in JSON at position 10 (line 1 column 11)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node message-check.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Version note
QuickJS, the engine of the Run button, also throws a SyntaxError, so the first check passes there too, but its
message is expecting property name, so the second check prints FAIL.
In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:
pass the error is a SyntaxError FAIL the message is the one V8 writes message here: expecting property name
The wording also moves between releases of one engine. The same JSON.parse('{"qty": 2,}'), run in five releases of
Node.js, gave three different messages:
| Node.js | Message |
|---|---|
| 18.20.2 | Unexpected token } in JSON at position 10 |
| 20.19.0 | Expected double-quoted property name in JSON at position 10 |
| 22.23.3, 24.21.0, 26.10.0 | Expected double-quoted property name in JSON at position 10 (line 1 column 11) |
A test that compared the message would have broken twice in two upgrades, and the Node.js documentation warns that
messages may change in any release. The console and reading error messages
shows how three browser engines word the same mistakes. In code and in tests, rely on the class, on name, and on a
documented code.
Errors that come from the host
Node.js and browsers throw errors of their own, and each identifies them in its own way.
// Errors from Node.js and from web APIs: identified by code and by name, not by message.
import { readFileSync } from 'node:fs';
function show(label, operation) {
try {
operation();
} catch (error) {
const kind = error instanceof DOMException ? 'DOMException' : error.constructor.name;
console.log(`${label}\n ${kind}, name ${error.name}, code ${JSON.stringify(error.code)}`);
}
}
show('read a file that does not exist', () => readFileSync('missing-config.json', 'utf8'));
show('make a Buffer from a number', () => Buffer.from(42));
show('make a Buffer of size -1', () => Buffer.alloc(-1));
show('parse a URL that is not one', () => new URL('not a url'));
show('copy a function with structuredClone', () => structuredClone(() => 1));
show('read why an aborted signal stopped', () => { throw AbortSignal.abort().reason; });
const selectorError = new DOMException('"[" is not a valid selector', 'SyntaxError');
console.log(`a DOMException named SyntaxError: instanceof SyntaxError ${selectorError instanceof SyntaxError}, instanceof Error ${selectorError instanceof Error}`); Output
read a file that does not exist Error, name Error, code "ENOENT" make a Buffer from a number TypeError, name TypeError, code "ERR_INVALID_ARG_TYPE" make a Buffer of size -1 RangeError, name RangeError, code "ERR_OUT_OF_RANGE" parse a URL that is not one TypeError, name TypeError, code "ERR_INVALID_URL" copy a function with structuredClone DOMException, name DataCloneError, code 25 read why an aborted signal stopped DOMException, name AbortError, code 20 a DOMException named SyntaxError: instanceof SyntaxError false, instanceof Error true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node host-errors.mjs
- Node.js system errors come from the operating system and carry a
codesuch asENOENT(no such file or folder),EACCES(permission denied) orEADDRINUSE(the port is taken), plussyscalland, for files,path. - Node.js’s own errors are ordinary
TypeError,RangeErrorandErrorobjects with acodethat starts withERR_, asERR_INVALID_ARG_TYPEandERR_OUT_OF_RANGEabove. The documentation callscodethe most stable way to identify an error: it changes only between major versions. DOMExceptionis the error of web APIs, and Node.js uses it for the same APIs:structuredClonethrows aDataCloneError, and an aborted signal’s reason is anAbortError. Itsnameis the identifier to check; the numericcodeis a legacy value that newer names do not have.
One name appears in both worlds. Web APIs that parse text, such as document.querySelector given an invalid
selector, throw a DOMException named SyntaxError, and the Web IDL standard warns against confusing it with
JavaScript’s SyntaxError. The last line of the example shows the trap: such an error is an Error, but not an
instance of SyntaxError. Check error instanceof DOMException && error.name === 'SyntaxError' for the first kind and
error instanceof SyntaxError for the second.
Exercise: the right error for each bad booking
Exercise · Easy · JavaScript
Throw the right error for each bad booking
validateBooking(input) checks a request for bus seats before it is sent on, such as { adults: 2, children: 1, seat: 'window' }, and returns it with the number of passengers added. Right now it throws a plain Error for every problem, so the code that calls it cannot tell a bug from a choice the traveller must fix. Make it throw the class that fits each problem:
inputis not an object (or isnull):TypeError;adultsorchildrenis not a number:TypeError;adultsorchildrenis a number but not a whole number of 0 or more, or there are not 1 to 6 passengers in total:RangeError;seatis not a string:TypeError; a string that is not'window','aisle'or'any':RangeError;- children without any adult: the
ValidationErrorgiven in the file, withfieldset to'adults'.
Give every error a message that names the bad value. The tests check the classes and field only, never the messages, so word them as you like.
Starter code · booking.mjs
/** A broken booking rule. It is given: throw it, do not change it. */
export class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
export const SEATS = ['window', 'aisle', 'any'];
export const MAX_PASSENGERS = 6;
/**
* Checks a bus booking request and returns it with the passenger count added.
* @param {{ adults: number, children: number, seat: string }} input
* @returns {{ adults: number, children: number, seat: string, passengers: number }}
*/
export function validateBooking(input) {
if (!input) throw new Error('no booking');
if (input.adults + input.children > MAX_PASSENGERS) throw new Error('too many passengers');
if (!SEATS.includes(input.seat)) throw new Error('unknown seat');
return { ...input, passengers: input.adults + input.children };
} The sample tests · booking.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { validateBooking, ValidationError } from './booking.mjs';
/** The error a call throws (the test fails when it throws nothing). */
function thrownBy(call) {
try {
call();
} catch (error) {
return error;
}
assert.ok(false, 'expected the call to throw');
}
test('a good request comes back with the passenger count', () => {
assert.deepEqual(validateBooking({ adults: 2, children: 1, seat: 'window' }), { adults: 2, children: 1, seat: 'window', passengers: 3 });
});
test('a request that is not an object is a TypeError', () => {
assert.ok(thrownBy(() => validateBooking(null)) instanceof TypeError);
assert.ok(thrownBy(() => validateBooking('2 adults')) instanceof TypeError);
});
test('counts that are not numbers are a TypeError', () => {
assert.ok(thrownBy(() => validateBooking({ adults: '2', children: 0, seat: 'any' })) instanceof TypeError);
});
test('counts that are numbers but not allowed are a RangeError', () => {
assert.ok(thrownBy(() => validateBooking({ adults: 1.5, children: 0, seat: 'any' })) instanceof RangeError, '1.5 adults');
assert.ok(thrownBy(() => validateBooking({ adults: 2, children: -1, seat: 'any' })) instanceof RangeError, '-1 children');
assert.ok(thrownBy(() => validateBooking({ adults: 4, children: 3, seat: 'any' })) instanceof RangeError, '7 passengers');
});
test('a seat that is not one of the choices is a RangeError, a seat that is not text a TypeError', () => {
assert.ok(thrownBy(() => validateBooking({ adults: 1, children: 0, seat: 'front' })) instanceof RangeError);
assert.ok(thrownBy(() => validateBooking({ adults: 1, children: 0, seat: 3 })) instanceof TypeError);
});
test('children without an adult break a booking rule: a ValidationError for the adults field', () => {
const error = thrownBy(() => validateBooking({ adults: 0, children: 2, seat: 'aisle' }));
assert.ok(error instanceof ValidationError, `expected a ValidationError, got ${error}`);
assert.equal(error.field, 'adults');
}); A hint
Check in the order the list gives: first the kind of value (typeof, and null, which typeof calls 'object'), then whether a number is allowed (Number.isInteger, the range), and the booking rule last. A small helper such as checkCount(value, field) saves writing the two count checks twice.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Self-check
Check yourself
7 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is the difference between TypeError and RangeError, and when should your own code throw each?
A TypeError means an operation could not work on the kind of value it got: calling something that is not a
function, reading a property of null, or, in your own code, receiving text where a number belongs. A RangeError
means the value was the right kind but not an allowed one: an array length of −1, 101 decimals for toFixed, a
quantity of 0, or a status string that is not in the accepted set. In your own functions, check the kind first and
throw a TypeError when it is wrong, then check the value and throw a RangeError when it is outside what you
accept. The difference helps the caller: a TypeError usually points at a bug in the calling code, while a
RangeError often describes a choice a person can correct.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
You maintain a payments library that 40 services use. A teammate wants to change the TypeError it throws for an amount given as text into a RangeError, to make the library consistent. What do you weigh before agreeing, and how would you ship the change?
-
Fresher to mid
A search box passes what users type to document.querySelector, and the code handles failures with
if (error instanceof SyntaxError) showHint(). Users who type "[" get no hint, and the error tracker reports a crash. Why, and how would you fix the handler?
Key takeaways
- The engine throws
TypeError,RangeError,ReferenceError,SyntaxError,URIErrorandAggregateError;EvalErrorremains only for old code, and engine limits such as deep recursion vary between engines. TypeErrorcovers the wrong kind of value;RangeErrorcovers a value of the right kind outside the allowed set or range, an unknown status string included.- Throw the same classes from your own functions, check the kind before the value, and give the rules of your application error classes of their own.
AggregateErrorreports several failures at once in itserrorslist;SuppressedErrorcomes withusing.- Messages differ between engines and between releases of one engine; test the class, the
nameor a documentedcode. - Node.js identifies system errors and its own errors by
code; web APIs throwDOMExceptions identified byname, and aDOMExceptionnamedSyntaxErroris not a JavaScriptSyntaxError.
References
- ECMAScript 2026 Language Specification: native error types used in this standard (Ecma International)
- ECMAScript 2026 Language Specification: AggregateError objects (Ecma International)
- ECMAScript 2026 Language Specification: Error.prototype.toString (Ecma International)
- Error (MDN Web Docs)
- InternalError (MDN Web Docs)
- Node.js 24 documentation: Errors (OpenJS Foundation (Node.js))
- Web IDL Standard: base DOMException error names (WHATWG)
- TC39 finished proposals (explicit resource management) (Ecma International (TC39))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress