Error Types and Custom Errors
Error, TypeError, RangeError, and custom subclasses — name, cause, stack, and catching by type without swallowing bugs.
- javascript
- errors
- Error
- custom-errors
Throwing strings is how you lose stacks and instanceof checks. The language gives you Error and a few subclasses; production apps add domain errors (ValidationError, HttpError) so catch can branch without parsing message text.
Built-ins you’ll see
| Type | Typical cause |
|---|---|
Error |
generic |
TypeError |
wrong type / null property access patterns |
RangeError |
length / size out of range |
SyntaxError |
JSON.parse, eval of bad source |
URIError |
bad decodeURIComponent |
ReferenceError |
missing variable |
JSON.parse('{'); // SyntaxError
new Array(-1); // RangeError
null.foo; // TypeError
Throwing properly
function divide(a, b) {
if (b === 0) throw new RangeError('division by zero');
return a / b;
}
try {
divide(1, 0);
} catch (err) {
if (err instanceof RangeError) {
// expected domain case
} else {
throw err; // rethrow unknowns
}
}
Custom errors
class AppError extends Error {
constructor(message, options = {}) {
super(message, options);
this.name = this.constructor.name;
}
}
class HttpError extends AppError {
constructor(status, message, options) {
super(message, options);
this.status = status;
}
}
class ValidationError extends AppError {
constructor(message, fields, options) {
super(message, options);
this.fields = fields;
}
}
throw new HttpError(404, 'User not found');
In modern engines, super(message, { cause }) chains errors:
try {
await fetchUser();
} catch (err) {
throw new AppError('profile load failed', { cause: err });
}
error.cause preserves the original stack context for logging.
name and stack
const err = new ValidationError('bad email', { email: 'required' });
err.name; // 'ValidationError'
err.message;
err.stack; // string, engine-specific
err instanceof ValidationError; // true
err instanceof Error; // true
Some older transpile targets needed Object.setPrototypeOf hacks for instanceof — native ES classes are fine in modern browsers.
Catch strategy
async function load() {
try {
return await api.get('/me');
} catch (err) {
if (err instanceof HttpError && err.status === 401) {
return redirectToLogin();
}
if (err instanceof ValidationError) {
return showFields(err.fields);
}
logUnexpected(err);
throw err;
}
}
- Catch narrow
- Rethrow what you don’t handle
- Don’t
catch (e) {}empty
instanceof fails across realms (iframes) — sometimes check err?.name === 'HttpError' as a fallback for serialized errors.
AggregateError
try {
await Promise.any([]);
} catch (e) {
if (e instanceof AggregateError) {
console.log(e.errors); // array of reasons
}
}
Interview answer
“I throw Error subclasses with a name and optional cause, not string literals. Built-ins like TypeError and SyntaxError signal language/runtime issues; custom HttpError/ValidationError carry domain fields. Catch by instanceof, handle what you can, rethrow the rest. cause chains underlying failures for logs.”
Related
Serializing errors across the wire
function toErrorDTO(err) {
return {
name: err.name,
message: err.message,
status: err.status ?? null,
fields: err.fields ?? null,
};
}
// client reconstruct
function fromErrorDTO(dto) {
if (dto.status) return Object.assign(new HttpError(dto.status, dto.message), dto);
return Object.assign(new Error(dto.message), { name: dto.name });
}
JSON.stringify(new Error('x')) is {} — message isn’t enumerable by default in many engines. Always pick fields explicitly for logs and APIs. Include cause chains in structured logs (one level at a time) so operators see root failures.
Further reading
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.