ApiError

Overview

services/api/src/utils/apiError.js defines a custom error class named ApiError. It extends the native Error type and adds API-friendly fields for statusCode, code, field, and details.

This class is designed for error objects that need more than a plain message. It keeps the standard error message behavior while carrying structured metadata for API responses or validation handling.

[!NOTE] ApiError always sets name to "ApiError", so it can be identified as a custom error type.

How it works

ApiError inherits from Error and runs its own constructor logic. The constructor stores the HTTP-style status code and evaluates the optional options argument.

If options is a plain object with at least one of these keys:

  • code
  • field
  • details

then the instance extracts structured values from it. Otherwise, the raw options value is stored in details, and code and field remain null.

Runtime flow

flowchart TB
    A[ApiError constructor] --> B[Call Error with message]
    B --> C[Set name to ApiError]
    C --> D[Store statusCode]
    D --> E{options is plain object}
    E -->|Yes and has code field or details| F[Extract code field details]
    E -->|No| G[Set code and field to null]
    F --> H[Set details from options keys]
    G --> I[Set details to raw options value]

The constructor keeps the native error message behavior intact. It then enriches the instance with API-oriented metadata for downstream handling.

Key components

{
    "title": "ApiError",
    "content": "Custom error class that extends Error and adds statusCode, code, field, and details metadata for API-oriented error handling.",
    "type": "class",
    "filePath": "services/api/src/utils/apiError.js",
    "badges": [
        "public"
    ]
}

Data & fields

[
    {
        "name": "statusCode",
        "type": "number",
        "required": true,
        "description": "Stored on the instance as the first constructor argument. It represents the error status code."
    },
    {
        "name": "message",
        "type": "string",
        "required": true,
        "description": "Passed to the parent Error constructor and used as the error message."
    },
    {
        "name": "options",
        "type": "object | any | null",
        "description": "Optional metadata passed to the constructor. When it is a plain object with structured keys, specific fields are extracted."
    },
    {
        "name": "name",
        "type": "string",
        "required": true,
        "description": "Set to 'ApiError' so the error can be identified by type."
    },
    {
        "name": "code",
        "type": "string | null",
        "description": "Set from options.code when structured options are provided. Otherwise it is null."
    },
    {
        "name": "field",
        "type": "string | null",
        "description": "Set from options.field when structured options are provided. Otherwise it is null."
    },
    {
        "name": "details",
        "type": "any",
        "description": "Stores options.details when structured options are provided. Otherwise it stores the raw options value."
    }
]

Constructor parameters

  • statusCode [badge:required] number — Stored directly on the error instance.
  • message [badge:required] string — Sent to Error and used as the error message.
  • options [badge:optional] object | any | null — Optional metadata container. It defaults to null.

Behaviors

  • constructor(statusCode, message, options = null) — Creates a new ApiError, assigns the message and status code, and conditionally extracts structured metadata.
  • super(message) — Initializes the base Error state with the provided message.
  • this.name = 'ApiError' — Labels the error instance with a stable custom name.
  • hasStructuredOptions detection — Treats options as structured only when it is a non-array object containing code, field, or details.
  • this.code assignment — Uses options.code when structured options exist; otherwise sets null.
  • this.field assignment — Uses options.field when structured options exist; otherwise sets null.
  • this.details assignment — Uses options.details when structured options exist; otherwise stores the raw options value.

Implementation details

The structured-options check is very specific:

const hasStructuredOptions =
  options &&
  typeof options === 'object' &&
  !Array.isArray(options) &&
  ('code' in options || 'field' in options || 'details' in options);

That logic means:

  • null does not count as structured.
  • Arrays do not count as structured.
  • Plain objects without the expected keys do not count as structured.
  • Any value can still be stored in details when the object is not structured.

When the object is structured, each field is normalized:

  • code becomes options.code || null
  • field becomes options.field || null
  • details becomes options.details ?? null

[!IMPORTANT] details behaves differently from code and field. If options is not a structured object, details keeps the original raw value.

Gotchas & edge cases

[!WARNING] A plain object without code, field, or details is not treated as structured metadata. In that case, code and field become null, and details receives the entire object.

[!CAUTION] details can contain a non-object value. When structured options are absent, the constructor stores the raw options argument unchanged.

[!NOTE] code and field use || null, so falsy values become null. details uses ?? null only for structured options, so undefined becomes null there.

ApiError is a reusable utility under services/api/src/utils/. It belongs to the API-side error handling layer as a custom error type with extra metadata, but this file does not show its callers or consumers.