Skip to main content

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:

CodeMeaning
ERR_MODERATION_INSUFFICIENT_ROLEThe requesting user's role is too low to perform this moderation action
ERR_BLOG_PUBLISH_DRAFT_ONLYOnly draft posts can be published; this post is already published
ERR_WEBHOOK_DUPLICATE_URLA webhook with this URL already exists for this site
ERR_FILE_UPLOAD_TYPE_NOT_ALLOWEDThe uploaded file type is not in the allowlist
ERR_AUTH_API_KEY_INVALIDThe 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_FOUND is better than ERR_BLOG_ERROR.
  • Do not add suffixes like _ERROR or _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.

CodeDomainHTTPi18n KeyAdded In
(codes are added here as they are created)