Issue #374 introduces explicit Result<T, E> handling for expected service failures.
Services should return Promise<Result<T>> for expected business outcomes:
const result = await projectService.getProject(id, tenantId);
if (!result.ok) return result;
return ok(result.value);Use Result errors for validation, not-found, forbidden, and conflict cases. Reserve thrown exceptions for unexpected runtime failures such as programming errors, dependency crashes, or unavailable infrastructure.
Controllers should map Result values to HTTP responses at the edge:
const result = await service.updateProject(id, body, tenantId);
this.sendResult(res, result, (project) => {
res.apiSuccess(project);
});This keeps business logic deterministic and avoids relying on exception control flow for normal client errors.
Shared primitives live in packages/types/src/exports.ts for SDK/API consumers and backend/src/lib/result.ts for backend runtime code. Use the following status conventions:
VALIDATION_ERROR: 400NOT_FOUND: 404FORBIDDEN: 403CONFLICT: 409INTERNAL_ERROR: 500 for unexpected failures only
- Change one service method at a time to return
Promise<Result<T>>. - Replace expected
throwcalls with typedvalidationFailure,notFoundFailure,forbiddenFailure, orconflictFailurehelpers. - Update controller call sites to use
sendResult. - Update tests to assert Result envelopes for service tests and HTTP envelopes for controller tests.
- Keep third-party library calls wrapped at service boundaries so library exceptions become
INTERNAL_ERRORonly when they are genuinely unexpected.