Skip to main content

Errors

Error types, codes, and HTTP status mappings.

ForgeError

The core error type in Rust. All function errors convert to this type.

use forge::ForgeError;

// Return an error from any function
Err(ForgeError::NotFound("User not found".to_string()))

Variants

VariantHTTP StatusError CodeUse Case
NotFound(String)404NOT_FOUNDResource does not exist
Unauthorized(String)401UNAUTHORIZEDAuthentication required or invalid
Forbidden(String)403FORBIDDENAuthenticated but insufficient permissions
Validation(String)400VALIDATION_ERRORInput validation failed
InvalidArgument(String)400INVALID_ARGUMENTMalformed or invalid arguments
Timeout(String)504TIMEOUTOperation exceeded time limit
RateLimitExceeded { retry_after, limit, remaining }429RATE_LIMITEDRate limit exceeded
Database(String)500INTERNAL_ERRORDatabase operation failed
Config(String)500INTERNAL_ERRORConfiguration error
Function(String)500INTERNAL_ERRORFunction execution failed
Job(String)500INTERNAL_ERRORJob execution failed
Cluster(String)500INTERNAL_ERRORCluster communication failed
Serialization(String)500INTERNAL_ERRORSerialization failed
Deserialization(String)500INTERNAL_ERRORDeserialization failed
Io(std::io::Error)500INTERNAL_ERRORIO operation failed
Sql(sqlx::Error)500INTERNAL_ERRORSQL execution failed
Internal(String)500INTERNAL_ERRORGeneric internal error
InvalidState(String)500INTERNAL_ERRORInvalid state transition
WorkflowSuspended--Internal signal for workflow suspension

RateLimitExceeded Fields

FieldTypeDescription
retry_afterDurationTime until next request allowed
limitu32Configured request limit
remainingu32Tokens remaining (0 when exceeded)

HTTP Response Format

All errors serialize to the same JSON shape:

{
"success": false,
"data": null,
"error": {
"code": "NOT_FOUND",
"message": "User not found",
"details": null
},
"requestId": "abc123"
}

Response Fields

FieldTypeDescription
successbooleanAlways false for errors
datanullAlways null for errors
error.codestringMachine-readable error code
error.messagestringHuman-readable description
error.detailsobject | nullAdditional context (optional)
requestIdstringRequest identifier for tracing

TypeScript Error Types

Generated client includes error types:

// types.ts
export interface ForgeError {
code: string;
message: string;
details?: Record<string, unknown>;
}

ForgeClientError

Thrown by API calls when the server returns an error:

import { ForgeClientError } from '$lib/forge';

try {
await getUser({ id });
} catch (e) {
if (e instanceof ForgeClientError) {
console.log(e.code); // "NOT_FOUND"
console.log(e.message); // "User not found"
console.log(e.details); // { userId: "..." }
}
}

Error Codes

CodeHTTP StatusDescription
NOT_FOUND404Requested resource does not exist
UNAUTHORIZED401Authentication required or token invalid
FORBIDDEN403User lacks required permissions
VALIDATION_ERROR400Input failed validation
INVALID_ARGUMENT400Argument format or value invalid
TIMEOUT504Request exceeded configured timeout
RATE_LIMITED429Too many requests
INTERNAL_ERROR500Server-side error
UNKNOWN500Unclassified error

Creating Custom Errors

Return Specific Variants

#[forge::query]
pub async fn get_user(ctx: &QueryContext, id: Uuid) -> Result<User> {
let user = sqlx::query_as("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_optional(ctx.db())
.await?;

user.ok_or_else(|| ForgeError::NotFound(format!("User {} not found", id)))
}

With Details

#[forge::mutation]
pub async fn create_order(ctx: &MutationContext, items: Vec<Item>) -> Result<Order> {
if items.is_empty() {
return Err(ForgeError::Validation("Order must contain at least one item".into()));
}

for item in &items {
if item.quantity == 0 {
return Err(ForgeError::InvalidArgument(
format!("Item {} has invalid quantity", item.id)
));
}
}

// Create order...
}

Role Enforcement

#[forge::query]
pub async fn get_audit_logs(ctx: &QueryContext) -> Result<Vec<AuditLog>> {
ctx.auth.require_role("admin")?; // Returns ForgeError::Forbidden if missing

// Fetch logs...
}

From Database Errors

#[forge::mutation]
pub async fn create_user(ctx: &MutationContext, email: String) -> Result<User> {
sqlx::query_as("INSERT INTO users (email) VALUES ($1) RETURNING *")
.bind(&email)
.fetch_one(ctx.db())
.await
.map_err(|e| {
if e.to_string().contains("unique constraint") {
ForgeError::Validation(format!("Email {} already exists", email))
} else {
ForgeError::Database(e.to_string())
}
})
}

Error Handling Patterns

Frontend: Per-Query Errors

Subscription stores expose error state:

<script lang="ts">
import { getUser$ } from '$lib/forge';
const user = getUser$({ id });
</script>

{#if user.loading}
<p>Loading...</p>
{:else if user.error}
{#if user.error.code === 'NOT_FOUND'}
<p>User not found</p>
{:else if user.error.code === 'FORBIDDEN'}
<p>You don't have access to this user</p>
{:else}
<p>Error: {user.error.message}</p>
{/if}
{:else}
<p>Welcome, {user.data.name}</p>
{/if}

Frontend: Mutation Errors

Catch errors from mutations:

async function submit() {
try {
await createUser({ email });
} catch (e) {
if (e instanceof ForgeClientError) {
if (e.code === 'VALIDATION_ERROR') {
showValidationError(e.message);
} else if (e.code === 'UNAUTHORIZED') {
redirectToLogin();
} else {
showGenericError(e.message);
}
}
}
}

Frontend: Global Auth Error Handler

Configure a handler for auth errors across all requests:

import { initForge } from '$lib/forge';

initForge({
url: 'http://localhost:3000',
getToken: () => localStorage.getItem('token'),
onAuthError: (error) => {
// Called when any request returns UNAUTHORIZED
localStorage.removeItem('token');
window.location.href = '/login';
}
});

Backend: Error Conversion

Implement From for domain errors:

pub enum OrderError {
InsufficientStock(String),
PaymentFailed(String),
ShippingUnavailable(String),
}

impl From<OrderError> for ForgeError {
fn from(e: OrderError) -> Self {
match e {
OrderError::InsufficientStock(msg) =>
ForgeError::Validation(format!("Insufficient stock: {}", msg)),
OrderError::PaymentFailed(msg) =>
ForgeError::Internal(format!("Payment failed: {}", msg)),
OrderError::ShippingUnavailable(msg) =>
ForgeError::Validation(format!("Shipping unavailable: {}", msg)),
}
}
}

Then use ? to propagate:

#[forge::mutation]
pub async fn create_order(ctx: &MutationContext, items: Vec<Item>) -> Result<Order> {
check_stock(&items).map_err(OrderError::InsufficientStock)?;
// Automatically converts to ForgeError::Validation
}

Rate Limit Errors

When rate limiting triggers, the error includes retry information:

ForgeError::RateLimitExceeded {
retry_after: Duration::from_secs(30),
limit: 100,
remaining: 0,
}

Handle on the frontend:

try {
await expensiveQuery();
} catch (e) {
if (e instanceof ForgeClientError && e.code === 'RATE_LIMITED') {
const retryAfter = e.details?.retry_after ?? 60;
showMessage(`Rate limited. Try again in ${retryAfter} seconds`);
}
}

sqlx::Error Conversion

SQLx errors automatically convert to ForgeError::Sql:

#[forge::query]
pub async fn get_user(ctx: &QueryContext, id: Uuid) -> Result<User> {
sqlx::query_as("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(ctx.db())
.await
.map_err(Into::into) // sqlx::Error -> ForgeError::Sql
}

To preserve the original error type for matching:

#[forge::query]
pub async fn get_user(ctx: &QueryContext, id: Uuid) -> Result<User> {
match sqlx::query_as("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(ctx.db())
.await
{
Ok(user) => Ok(user),
Err(sqlx::Error::RowNotFound) =>
Err(ForgeError::NotFound(format!("User {} not found", id))),
Err(e) => Err(e.into()),
}
}

Error Logging

All errors are logged server-side with context:

ERROR forge::function: Function error
function=get_user
request_id=abc-123
user_id=Some(user-456)
error="User not found"
error_code=NOT_FOUND

Client errors (4xx) log at WARN level. Server errors (5xx) log at ERROR level.