ESC

Type to search the knowledge base.

Error Types and Custom Errors

Error, TypeError, RangeError, and custom subclasses — name, cause, stack, and catching by type without swallowing bugs.

beginner3 min read
  • 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.”

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