Skip to content

Feat/centralized error handling#112

Merged
josephchimebuka merged 12 commits intoTevaLabs:mainfrom
KevinMB0220:feat/centralized-error-handling
Mar 25, 2026
Merged

Feat/centralized error handling#112
josephchimebuka merged 12 commits intoTevaLabs:mainfrom
KevinMB0220:feat/centralized-error-handling

Conversation

@KevinMB0220
Copy link
Contributor

Summary

  • Introduces a typed error class hierarchy (AppError + 8 subclasses) replacing ad-hoc Error objects scattered across routes and services
  • Adds a single errorHandler Express middleware that owns all error-to-HTTP mapping, structured logging, and Prisma error translation
  • All routes now propagate errors via next(error) instead of responding inline, eliminating ~140 lines of duplicated error handling code

Changes

New files

  • src/utils/errors.tsAppError base class + ValidationError (400), AuthenticationError (401), AuthorizationError (403), NotFoundError (404), ConflictError (409), BusinessRuleError (422), ExternalServiceError (503), ConfigurationError (500)
  • src/middleware/errorHandler.middleware.ts — Centralized error handler; maps AppError subclasses, translates Prisma errors (P2025→404, P2002→409), falls back to 500; exports asyncHandler wrapper
  • src/tests/errorHandler.spec.ts — 18 tests covering all error classes and middleware HTTP mapping

Modified files

  • src/middleware/validate.middleware.ts — Calls next(new ValidationError(...)) instead of responding directly
  • src/services/round.service.ts — Throws ConflictError("...", "ACTIVE_ROUND_EXISTS") instead of ad-hoc error object
  • src/routes/* (8 files) — All catch blocks use next(error); inline 404/401 guards use typed error classes
  • src/index.ts — Replaces inline global error handler with errorHandler middleware
  • src/docs/openapi.tsErrorResponse schema updated to document the full standardized shape

Error response contract

{
  "error": "ValidationError",
  "message": "walletAddress is required",
  "code": "VALIDATION_ERROR",
  "details": [{ "field": "walletAddress", "message": "walletAddress is required" }]
}
Error Class HTTP Code example
ValidationError 400 VALIDATION_ERROR
AuthenticationError 401 INVALID_SIGNATURE, CHALLENGE_EXPIRED
AuthorizationError 403 AUTHORIZATION_ERROR
NotFoundError 404 NOT_FOUND
ConflictError 409 ACTIVE_ROUND_EXISTS
BusinessRuleError 422 INVALID_ROUND_STATE
ExternalServiceError 503 EXTERNAL_SERVICE_ERROR
Prisma P2025 404 NOT_FOUND
Prisma P2002 409 CONFLICT
Unknown Error 500 INTERNAL_ERROR

Test plan

  • npm run lint passes (no type errors)
  • New errorHandler.spec.ts — 18 tests, all green
  • Existing auth.routes.spec.ts — all tests pass (updated mocks + error field assertions)
  • Existing notifications.routes.spec.ts — all tests pass
  • Trigger validation error → verify { error: "ValidationError", code: "VALIDATION_ERROR", details: [...] }
  • Trigger auth error → verify { error: "AuthenticationError", code: "INVALID_SIGNATURE" }
  • Start duplicate round → verify 409 with code: "ACTIVE_ROUND_EXISTS"
  • Hit unknown route → verify 404 with code: "NOT_FOUND"

Closes #92

KevinMB0220 and others added 12 commits March 23, 2026 13:34
Introduces AppError base class and typed subclasses:
ValidationError (400), AuthenticationError (401),
AuthorizationError (403), NotFoundError (404),
ConflictError (409), BusinessRuleError (422),
ExternalServiceError (503), ConfigurationError (500).

Closes TevaLabs#92

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
errorHandler maps AppError subclasses, Prisma known errors
(P2025→404, P2002→409), and unknown errors to consistent
HTTP responses with structured logging. Also exports
asyncHandler for zero-boilerplate async route wrappers.

Closes TevaLabs#92

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Instead of responding directly, the middleware now calls
next(new ValidationError(...)) so the centralized error
handler owns all error responses.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Replaces ad-hoc error object with error.code = 'ACTIVE_ROUND_EXISTS'
with a typed ConflictError so the centralized handler returns
409 automatically.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
All route catch blocks now call next(error) instead of
responding directly. Inline 401/404 guards use typed error
classes (AuthenticationError, NotFoundError, etc.) so the
centralized handler owns the full error response lifecycle.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Replaces the inline global error handler with the new
errorHandler middleware. Updates the 404 handler to return
the standardized error shape.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
ErrorResponse now documents error (class name), message,
code (machine-readable), and optional details array so API
consumers can rely on a single documented error shape.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
18 tests covering:
- All AppError subclass properties (statusCode, code, instanceof)
- errorHandler HTTP mapping for each error type
- Unknown Error → 500 fallback
- Stack trace excluded in production
- 404 handler shape via createApp integration

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Add missing authChallenge.updateMany mock to auth route tests
- Update error field assertions from human-readable labels
  (e.g. "Validation Error") to class names ("ValidationError")
- Update notifications 404 assertions to check message field

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The package ships as ESM ("type": "module") but the backend compiles to
CJS. A static import would be compiled to require() and fail at runtime
with ERR_REQUIRE_ESM.

- Use `import type` for compile-time types (zero runtime cost)
- Move Client instantiation to a private async init() stored as this.ready
- Use `await import()` inside init() — works across ESM/CJS boundary
- Make ensureInitialized() async so all methods await this.ready
- Delete src/types/xelma-bindings.d.ts — real package types are now used

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
prediction.service.spec.ts: jest.mock() is hoisted before const
declarations, causing TDZ ReferenceError. Fix by creating jest.fn()
instances inside the factory and getting named references from the
mocked module after import.

prediction.service.ts: move mode-specific validation (side/priceRange)
before any DB writes, and add an explicit user.findUnique check so
"User not found" is distinguished from "Insufficient balance".

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
prediction.service.spec.ts:
- Keep HEAD factory (includes user.findUnique needed by updated service)
- Use exact where-clause assertion from origin/main for atomic update
- Add mockUserFindUnique setup to success cases

auth.routes.spec.ts:
- Keep mockAuthChallengeUpdate (service calls authChallenge.update at line 360)
- Keep ValidationError shape (centralized error handler)
- Keep findUnique mock in success test (route re-fetches challenge after updateMany)
- Keep explanatory comments
@josephchimebuka josephchimebuka merged commit 17eda8b into TevaLabs:main Mar 25, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add centralized error handling and recovery strategy

2 participants