Skip to content

Error Handling

The backend returns structured error responses with a stable code field. The frontend uses this code as the i18n key to display localized error messages.


1. AppException — The Only Throw Pattern

All errors are thrown via AppException. Never throw NestJS HttpException subclasses (NotFoundException, ConflictException, etc.) — use AppException with the appropriate ErrorCode instead.

import { AppException, ErrorCode } from '../common';
import { HttpStatus } from '@nestjs/common';

// Simple — code + message + status is sufficient
throw new AppException(ErrorCode.AUTH_TOKEN_INVALID, 'Invalid token', HttpStatus.UNAUTHORIZED);

// With params — frontend i18n interpolation
throw new AppException(ErrorCode.NOT_FOUND, 'Student not found', HttpStatus.NOT_FOUND, {
  params: { entity: 'student' },
});

// With data — structured error payload (import pipeline, validation)
throw new AppException(ErrorCode.IMPORT_VALIDATION_FAILED, 'Import validation failed', HttpStatus.UNPROCESSABLE_ENTITY, {
  data: { errors },
});

Rules: - Always use AppException — never throw new NotFoundException() etc. - Never catch exceptions just to re-throw — let AllExceptionsFilter handle formatting. - Every error carries a stable code for frontend i18n keying. - New codes must be added to the ErrorCode enum (src/common/constants/error-codes.ts). - Use UPPER_SNAKE_CASE for all error codes.


2. Response Shape

Every error response has at least these 5 base fields:

{
  "statusCode": 404,
  "code": "NOT_FOUND",
  "message": "Student not found",
  "timestamp": "2026-03-10T12:00:00.000Z",
  "path": "/api/v1/students/abc-123"
}

Some error codes add typed params or data fields — see Typed Swagger Schemas below.

Field Description
statusCode HTTP status code
code Stable, machine-readable string. Frontend uses this as i18n key
message English human-readable string for logs/debugging
params Optional typed object — interpolation values for the frontend translation template (only on specific DTOs)
data Optional typed payload — structured details like validation errors (only on specific DTOs)
timestamp ISO 8601 timestamp
path Request URL path

Frontend translation example:

{
  "NOT_FOUND": "{{entity}} not found",
  "SETUP_STEP_MISMATCH": "Expected step {{expected}}, received {{received}}",
  "AUTH_TOKEN_REUSE": "Security alert: session was reused. Please log in again."
}


3. Error Code Registry

Generic Error Codes

Code Status Params Description
INTERNAL_ERROR 500 Catch-all for unhandled server errors. Real error logged server-side, never leaked.
VALIDATION_FAILED 400 Class-validator failures or custom field validation. See Validation Errors below.
RATE_LIMITED 429 Rate limit exceeded. Global: 10 req/60s. Login: 5 req/60s.
HTTP_<status> varies Fallback for standard NestJS HttpException subclasses without a custom code.

Authentication Error Codes

Code Status Params When
INVALID_CREDENTIALS 401 Email exists but password does not match.
AUTH_TOKEN_INVALID 401 JWT expired, malformed, or revoked. Refresh token invalid/expired. User record not found.
AUTH_TOKEN_REUSE 401 Refresh token replay detected — entire token family revoked. Frontend should force re-login.
AUTH_REFRESH_MISSING 401 No refresh token cookie in request.
AUTH_TENANT_INVALID 401 Invalid tenant selection during multi-tenant login.
ACTIVE_PROFILE_NOT_AVAILABLE 400 / 401 The active profile selected/switched to is not available for this user. 400 on select-profile / switch-profile with a profile the user doesn't hold; 401 on refresh when the stored profile is missing from the user's current DB rows, or when the refresh-token row pre-dates the activeProfile column (legacy NULL value).
TENANT_SUSPENDED 403 Tenant is not active (suspended or trial-expired).
USER_DEACTIVATED 403 User account is deactivated.

Permission Error Codes

Code Status Params When
INSUFFICIENT_SCOPE 403 User lacks the required scope+action for the route (ScopeGuard).
ACTION_NOT_PERMITTED 403 User does not have the required action permission (ActionGuard).
NO_AUTHENTICATED_USER 403 Permission guard ran without authenticated user. Configuration error.
FORBIDDEN_FIELDS 403 Write request contains scope groups outside the user's writable scopes (FieldWriteGuard).

Database Error Codes

Code Status Params When
NOT_FOUND 404 { entity } Record not found. entity is the lowercased model/entity name (e.g. "student", "teacher").
CONFLICT 409 { entity } Unique constraint violation. entity from Prisma model name when available.
FOREIGN_KEY_VIOLATION 400 Foreign key constraint failed.
NULL_CONSTRAINT_VIOLATION 400 A required (non-nullable) field received null.
REQUIRED_RELATION_VIOLATION 400 A required relation is missing.
DATABASE_TIMEOUT 503 Database connection pool timeout. Clients should retry.
SCHEMA_OUT_OF_SYNC 500 Table/column missing or stored value incompatible with column type (Prisma P2021/P2022/P2023). Run pending migrations.
DATABASE_ERROR 400 Unmapped Prisma error code.

Academic Year Error Codes

Code Status Params When
ACADEMIC_YEAR_NOT_FOUND 404 Provided academicYearId not found or doesn't belong to the tenant (or is DELETED).
NO_ACTIVE_ACADEMIC_YEAR 409 No ACTIVE academic year exists for the tenant. Tenant hasn't completed setup.
ACADEMIC_YEAR_ARCHIVED 409 Mutation attempted on a record belonging to an archived academic year. Create, update, delete, and import are blocked.

Setup Wizard Error Codes

Code Status Params When
SETUP_STEP_MISMATCH 409 { expected, received } Frontend's currentStep doesn't match backend state. User should reload.
SETUP_INVALID_NAVIGATION 400 Cannot navigate to target step (only backward, same, or next allowed).
SETUP_DATA_REQUIRED 400 Data is required for forward navigation.
SETUP_STEP_INCOMPLETE 400 { step } Step not complete, can't advance.
SETUP_VALIDATION_FAILED 400 { reason, field? } Step-specific business rule validation failed. See reason values below.

SETUP_VALIDATION_FAILED reason values

Frontend uses compound key SETUP_VALIDATION_FAILED.{reason} for translation:

reason When Example field
date_range_invalid startDate >= endDate academicYear, periods
period_out_of_bounds period outside year range periods
overlap overlapping periods of same type periods
duplicate_name non-unique name (case-insensitive) departments, grades, periods, roomTypes
ordinal_gap non-sequential ordinal positions departments, grades
missing_departments not all tenant depts represented departments
missing_year no academic year found
invalid_department department not owned by tenant departments, departmentId
unknown_room_type room references a room type not in submission roomTypeName
canteen_missing_shifts canteen room has no lunch shifts lunchShifts
non_canteen_has_shifts non-canteen room has lunch shifts lunchShifts
shift_invalid_range lunch shift endTime <= startTime lunchShifts
shift_overlap overlapping lunch shifts in same room lunchShifts
delete_standard_type attempt to delete a built-in room type roomTypes
rename_standard_type attempt to rename a built-in room type roomTypes
unresolved_room_type room type not found after sync roomTypeName
duplicate unique constraint violation (belt-and-suspenders) rooms, curricula, studyPlans
invalid_grade grade does not belong to specified department gradeIds
invalid_curriculum curriculum not owned by tenant curriculumId
grade_not_in_curriculum grade not linked to curriculum via CurriculumGrade gradeId
missing_hours neither yearLessons nor weeklyLessons provided subjects, option blocks
min_exceeds_max option block minSelections > maxSelections optionBlocks
max_exceeds_subjects maxSelections > number of subjects in block optionBlocks

Import Validation Error Codes

Top-level envelope uses IMPORT_VALIDATION_FAILED (422). Each error inside data.errors[] carries an ImportErrorCode for i18n.

{
  "statusCode": 422,
  "code": "IMPORT_VALIDATION_FAILED",
  "message": "Import validation failed",
  "data": {
    "errors": [
      { "code": "FIELD_INVALID", "column": "gender", "rows": "2-5, 8", "allowedValues": ["MALE", "FEMALE"] },
      { "code": "HEADERS_MISSING", "column": "headers", "rows": "1", "params": ["last_name"], "allowedValues": ["first_name", "last_name"] }
    ]
  }
}
Code Description
HEADERS_MISSING Expected columns not found. params lists missing names.
FIELD_REQUIRED A required field is empty.
FIELD_MAX_LENGTH Value exceeds maximum length.
FIELD_INVALID Value doesn't match allowed options. allowedValues lists valid options.

Validation Errors

Produced by the global ValidationPipe (class-validator). Returns structured field-level errors:

{
  "statusCode": 400,
  "code": "VALIDATION_FAILED",
  "message": "Validation failed",
  "data": {
    "errors": [
      { "field": "email", "rule": "isEmail" },
      { "field": "anagraphic.firstName", "rule": "isNotEmpty" },
      { "field": "anagraphic.firstName", "rule": "maxLength", "params": { "max": "100" } }
    ]
  }
}

Each error has: - field — dot-notation path to the field - rule — class-validator constraint name (e.g. isEmail, isNotEmpty, maxLength, minLength) - params — optional constraint parameters for interpolation (e.g. { max: "100" })

Frontend maps rule to a translated message: validation.isEmail → "Must be a valid email address".

Custom Fields Error Codes

Custom field validation errors use VALIDATION_FAILED with English messages. Custom field CRUD errors:

Code Status When
VALIDATION_FAILED 400 Wrong type, unknown field key, missing required field, invalid SELECT value, SELECT missing options, duplicate options.
CONFLICT 409 Duplicate fieldKey within same tenant+entity.
FORBIDDEN_FIELDS 403 User lacks WRITE access on target entity+scope.
NOT_FOUND 404 Definition not found, or scope not found in permission catalogue.

4. Typed Swagger Schemas

Swagger documents 7 distinct error response shapes. Each is a DTO class in src/common/dto/ — the base carries no params/data, subclasses add strongly typed fields.

# DTO Codes params type data type
1 ErrorResponseDto (base) 20 simple codes (AUTH_*, RATE_LIMITED, INSUFFICIENT_SCOPE, etc.)
2 EntityErrorResponseDto NOT_FOUND, CONFLICT EntityErrorParams { entity }
3 StepMismatchErrorResponseDto SETUP_STEP_MISMATCH StepMismatchErrorParams { expected, received }
4 StepIncompleteErrorResponseDto SETUP_STEP_INCOMPLETE StepIncompleteErrorParams { step }
5 SetupValidationErrorResponseDto SETUP_VALIDATION_FAILED SetupValidationErrorParams { reason, field? }
6 ValidationErrorResponseDto VALIDATION_FAILED ValidationErrorData { errors: ValidationFieldErrorDto[] }
7 ImportErrorResponseDto IMPORT_VALIDATION_FAILED ImportValidationData { errors: ImportValidationErrorDto[] }

Source files: - Base + params: src/common/dto/error-response.dto.ts, src/common/dto/error-params.dto.ts - Subclasses: src/common/dto/typed-error-responses.dto.ts - Validation data: src/common/dto/validation-error-response.dto.ts, src/common/dto/import-validation-error.dto.ts

Runtime note: The AllExceptionsFilter continues to produce plain JSON objects. The typed DTOs are purely Swagger schema documentation — the filter's output already matches the DTO structures.


5. AppException Constructor

class AppException<TData = undefined> extends HttpException {
  constructor(
    code: ErrorCode,
    message: string,
    statusCode?: HttpStatus,          // default: 500
    options?: { data?: TData; params?: Record<string, string> },
  );
}

Source: src/common/exceptions/app.exception.ts