Error Codes
Forja uses a structured error code system. Every error path in the backend has a unique, stable identifier that maps to a user-facing localized message on the frontend.
Error codes are a product feature. They enable precise error handling, clear user messaging, and targeted regression tests.
Format
ERR_<DOMAIN>_<CAUSE>
DOMAIN— the feature area or resource (uppercase, no spaces)CAUSE— what went wrong (uppercase, descriptive, specific)
Examples:
| Code | Meaning |
|---|---|
ERR_MODERATION_INSUFFICIENT_ROLE | The requesting user's role is too low to perform this moderation action |
ERR_BLOG_PUBLISH_DRAFT_ONLY | Only draft posts can be published; this post is already published |
ERR_WEBHOOK_DUPLICATE_URL | A webhook with this URL already exists for this site |
ERR_FILE_UPLOAD_TYPE_NOT_ALLOWED | The uploaded file type is not in the allowlist |
ERR_AUTH_API_KEY_INVALID | The provided API key does not match any active key |
Rules:
- Once a code is shipped, it does not change. Clients and tests depend on it.
- Use specific causes —
ERR_BLOG_NOT_FOUNDis better thanERR_BLOG_ERROR. - Do not add suffixes like
_ERRORor_FAILURE— the name already implies an error context.
Backend to Frontend Flow
Error codes travel from the Rust backend to the React frontend through a defined pipeline:
Step 1 — Backend: ApiError variant
Every error path in a handler returns a dedicated ApiError variant with a unique code:
#[derive(Debug)]
pub enum ApiError {
// ...existing variants...
WebhookDuplicateUrl,
}
impl ApiError {
pub fn code(&self) -> &'static str {
match self {
ApiError::WebhookDuplicateUrl => "ERR_WEBHOOK_DUPLICATE_URL",
// ...
}
}
}
Step 2 — Backend: ProblemDetails response (RFC 7807)
The error is serialized as a ProblemDetails response with the code in the type field:
{
"type": "ERR_WEBHOOK_DUPLICATE_URL",
"title": "Duplicate webhook URL",
"status": 422,
"detail": "A webhook with this URL already exists for this site."
}
Step 3 — Frontend: error code mapping
The frontend reads response.data.type (the error code) and maps it to a handler:
try {
await apiService.createWebhook(data);
} catch (error) {
const code = error.response?.data?.type;
if (code === 'ERR_WEBHOOK_DUPLICATE_URL') {
setError('url', { message: t('errors.webhook.duplicateUrl') });
} else {
showErrorSnackbar(t('errors.generic'));
}
}
Step 4 — Frontend: i18n message
The error code maps to a localized string in admin/src/i18n/locales/en.json:
{
"errors": {
"webhook": {
"duplicateUrl": "A webhook with this URL already exists. Each site can only have one webhook per URL."
}
}
}
The same key must be added to all 11 locale files.
Adding a New Error Code
Follow this four-step process when introducing a new failure path:
Step 1 — Define the code
Choose a code following ERR_<DOMAIN>_<CAUSE>. Document it in the issue's Error Cases table before writing any code.
Step 2 — Add the ApiError variant (backend)
Add a new variant to the ApiError enum in the backend codebase. Add the matching string in the code() method. Return this error from the handler where the failure occurs.
Step 3 — Add the i18n key (frontend)
Add the key to admin/src/i18n/locales/en.json under the appropriate namespace:
{
"errors": {
"<domain>": {
"<cause in camelCase>": "<Human-readable message that explains what happened and, where possible, what the user can do.>"
}
}
}
Then add the same key (with appropriate translations or English placeholder) to all other 10 locale files.
Step 4 — Handle in the frontend
In the component or hook that calls the affected endpoint, map the error code to the i18n key. Distinguish between user-fixable errors (validation, permissions — show inline or in a field-level error) and system errors (500s — show a generic error snackbar).
Then add the code to the registry table below.
Error Code Registry
This table is the canonical source of all error codes in Forja. Add a row when you add a new code. Do not remove or rename existing rows.
| Code | Domain | HTTP | i18n Key | Added In |
|---|---|---|---|---|
| (codes are added here as they are created) |