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]
ApiErroralways setsnameto"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:
codefielddetails
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 toErrorand used as the error message.options[badge:optional]object | any | null— Optional metadata container. It defaults tonull.
Behaviors
constructor(statusCode, message, options = null)— Creates a newApiError, assigns the message and status code, and conditionally extracts structured metadata.super(message)— Initializes the baseErrorstate with the provided message.this.name = 'ApiError'— Labels the error instance with a stable custom name.hasStructuredOptionsdetection — Treatsoptionsas structured only when it is a non-array object containingcode,field, ordetails.this.codeassignment — Usesoptions.codewhen structured options exist; otherwise setsnull.this.fieldassignment — Usesoptions.fieldwhen structured options exist; otherwise setsnull.this.detailsassignment — Usesoptions.detailswhen structured options exist; otherwise stores the rawoptionsvalue.
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:
nulldoes 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
detailswhen the object is not structured.
When the object is structured, each field is normalized:
codebecomesoptions.code || nullfieldbecomesoptions.field || nulldetailsbecomesoptions.details ?? null
[!IMPORTANT]
detailsbehaves differently fromcodeandfield. Ifoptionsis not a structured object,detailskeeps the original raw value.
Gotchas & edge cases
[!WARNING] A plain object without
code,field, ordetailsis not treated as structured metadata. In that case,codeandfieldbecomenull, anddetailsreceives the entire object.
[!CAUTION]
detailscan contain a non-object value. When structured options are absent, the constructor stores the rawoptionsargument unchanged.
[!NOTE]
codeandfielduse|| null, so falsy values becomenull.detailsuses?? nullonly for structured options, soundefinedbecomesnullthere.
Related
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.