Exceptions
Learn about Spiderly's exception types and how the global exception handler maps them to HTTP responses.
Spiderly's global exception handler catches all exceptions and maps them to appropriate HTTP responses. Responses share a common ApiErrorDTO shape:
{
statusCode: number;
message: string;
errorCode?: string; // machine-readable discriminator (e.g. "invalid_token")
fieldErrors?: { [field: string]: string[] }; // populated for 422 responses
exception?: string; // full stack trace, development only
}The errorCode discriminator is one of these machine-readable values (generated from the backend ApiErrorCodes contract):
| Name | Value | Description |
|---|---|---|
ConcurrencyConflict | concurrency_conflict | An optimistic-concurrency check failed — the row was modified by someone else after it was loaded. The client should reload the latest data and retry. |
EmailNotVerified | email_not_verified | Login or auto-provisioning was blocked because the account's email address is not verified (e.g. an external provider returned an unverified email). |
ExternalEmailMissing | external_email_missing | An external (OAuth/OIDC) login was validated but the provider returned no email address (e.g. the user declined the email permission, or a phone-only Facebook account). Auto-provisioning needs an email to key the account on, so login is rejected with this code and the client should route the user to another sign-in method. Distinct from EmailNotVerified, which means an email was returned but not verified. |
ExternalProviderNotConfigured | external_provider_not_configured | An external (OAuth) login was attempted for a provider that is not configured on the server, or that provider's token exchange failed. |
ForeignKeyViolation | foreign_key_violation | A database foreign-key constraint was violated — e.g. referencing a row that does not exist, or deleting a row that is still referenced by dependent rows. |
InvalidToken | invalid_token | The JWT bearer token is missing, malformed, or expired. Returned with HTTP 401 (also surfaced in the WWW-Authenticate header); the client should refresh the token or re-authenticate. |
UniqueViolation | unique_violation | A database unique constraint (or unique index) was violated — e.g. saving a duplicate value for a column that must be unique. |
ValidationFailed | validation_failed | One or more request fields failed server-side validation. Returned with HTTP 400; the per-field messages are carried in ApiErrorDTO.FieldErrors. |
| Exception | Status | Log Level | Message to Client | Notification |
|---|---|---|---|---|
BusinessException | 400 | Information | Exception message | No |
ExpiredVerificationException | 400 | Information | Exception message | No |
SpiderlyValidationException | 422 | Information | Per-field errors | No |
FluentValidation.ValidationException | 422 | Information | Per-field errors | No |
UnauthorizedException | 401 | Warning | Exception message | No |
SecurityTokenException | 401 (RFC 6750) | Information | Exception message | No |
SecurityViolationException | 403 | Error | Generic error | Yes |
DbUpdateConcurrencyException | 409 | Warning | Localized ConcurrencyException | No |
DbUpdateException (unique/FK violation) | 409 | Warning | Localized message | No |
| Unhandled | 500 | Error | Generic error | Yes |
BusinessException
The primary way to reject invalid input with a user-facing message. Throw it from lifecycle hooks (e.g., OnBeforeProductInsert) to return a meaningful error to the client:
throw new BusinessException("Product name must be unique.");Always returns 400 Bad Request. Use a different exception type if you need a different status — that's the whole point of semantic exceptions.
ExpiredVerificationException
Returns 400 Bad Request with the exception message, logged at Information level. Used internally by the security module when a verification code has expired.
SpiderlyValidationException
Throw when domain rules produce per-field errors that can't be expressed via FluentValidation attributes. Returns 422 Unprocessable Entity with a fieldErrors dictionary the client can render inline next to the offending inputs:
throw new SpiderlyValidationException(new Dictionary<string, string[]>
{
["Sku"] = new[] { "SKU already exists." }
});Standard FluentValidation failures (ValidateAndThrow() from generated validators) are converted to the same response shape automatically — you don't need to catch and rethrow.
UnauthorizedException
For custom authorization checks — returns 401 Unauthorized with your message. See the Authorization Service section for a usage example.
SecurityTokenException
Thrown from refresh-token flows when the presented token is missing, expired, or tampered with. Returns 401 Unauthorized with:
WWW-Authenticate: Bearer error="invalid_token", error_description="..."(per RFC 6750)errorCode: "invalid_token"in the body
The handler also clears the access, refresh, and auth-result cookies. Clients should check either the header or the errorCode and treat the response as a forced logout.
SecurityViolationException
For requests that indicate malicious intent (tampered IDs, forged hidden fields, file-size bypass attempts). Returns a generic error message (never revealing what went wrong) and triggers an admin notification so you can investigate. Used internally by Spiderly's generated services for integrity checks.
DbUpdateException
The handler recognises common Postgres and SQL Server constraint violations and maps them to 409 Conflict with localized messages:
| Dialect | Code | Meaning | Localization key |
|---|---|---|---|
| Postgres | 23505 | Unique violation | UniqueConstraintException |
| Postgres | 23503 | Foreign-key violation | ForeignKeyConstraintException |
| SQL Server | 2627, 2601 | Unique violation | UniqueConstraintException |
| SQL Server | 547 | Foreign-key violation | ForeignKeyConstraintException |
Other DbUpdateException instances fall through to the generic 500 handler. DbUpdateConcurrencyException is handled separately with the ConcurrencyException message.
Unhandled Exceptions
Any exception that doesn't match the types above returns a generic error message to the client (never exposing internal details) and triggers an admin notification in production via the notification framework (INotifier → the Email channel).
How errors surface in the admin UI
The Angular admin handles failed requests globally — there is no per-call error wiring. An HTTP-error interceptor shows the right toast and rethrows, so calling code only ever runs its success path:
| Response | What the user sees |
|---|---|
| Connection failure (status 0) | "Server lost connection" warning |
400 | Warning toast with the server's message (your BusinessException text) |
401 | Session cleared on an invalid token (guards redirect to login); otherwise a "login required" warning |
403 | Permission warning |
404 | Not-found warning |
| Anything else | Generic error toast |
Uncaught errors that are not HTTP responses get a generic toast from the global Angular ErrorHandler. The practical rule: throw the right exception server-side and the UI story is done — add client-side catchError only to react to a failure (e.g. reset local state), never to show the message, which has already been shown by the time your handler runs.