Error Handling
Two error types drive extension loading: LoaderError for dependency, schema, migration, and path failures, and ConfigError for configuration validation.
On this page
The extension system defines two primary error types: LoaderError for extension loading and ConfigError for configuration validation.
LoaderError
Errors during extension registration, validation, and initialization:
#[derive(Debug, Error)]
pub enum LoaderError {
#[error("Extension '{extension}' requires dependency '{dependency}' which is not registered")]
MissingDependency {
extension: String,
dependency: String,
},
#[error("Extension with ID '{0}' is already registered")]
DuplicateExtension(String),
#[error("Failed to initialize extension '{extension}': {message}")]
InitializationFailed { extension: String, message: String },
#[error("Failed to install schema for extension '{extension}': {message}")]
SchemaInstallationFailed { extension: String, message: String },
#[error("Migration failed for extension '{extension}': {message}")]
MigrationFailed { extension: String, message: String },
#[error(
"Migration {version} for extension '{extension}' is not reversible (no down SQL provided)"
)]
MigrationNotReversible { extension: String, version: u32 },
#[error("Configuration validation failed for extension '{extension}': {message}")]
ConfigValidationFailed { extension: String, message: String },
#[error("Extension '{extension}' uses reserved API path '{path}'")]
ReservedPathCollision { extension: String, path: String },
#[error("Extension '{extension}' has invalid base path '{path}': must start with /api/")]
InvalidBasePath { extension: String, path: String },
#[error("Dependency cycle detected while ordering extensions: {chain}")]
DependencyCycle { chain: String },
}
Additional variants cover schema ownership and seed validation: CrossExtensionAlterUndeclared, DuplicateTableOwner, CrossExtensionTableNotOwned, InvalidSeedStatement, SeedInsertNotIdempotent, and SeedFailed.
Handling LoaderError
Extensions are registered through the typed ExtensionBuilder, whose build() returns Result<TypedExtensionRegistry, LoaderError>:
match ExtensionBuilder::new()
.extension(UsersExtension)
.extension(OauthExtension)
.build()
{
Ok(registry) => {
// Proceed with startup
}
Err(LoaderError::MissingDependency { extension, dependency }) => {
eprintln!("Extension '{}' requires '{}' - ensure it's registered first", extension, dependency);
std::process::exit(1);
}
Err(LoaderError::DependencyCycle { chain }) => {
eprintln!("Dependency cycle: {}", chain);
eprintln!("Break the cycle by restructuring dependencies");
std::process::exit(1);
}
Err(e) => {
eprintln!("Extension error: {}", e);
std::process::exit(1);
}
}
ConfigError
Errors during configuration validation:
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Configuration key '{0}' not found")]
NotFound(String),
#[error("Invalid configuration value for '{key}': {message}")]
InvalidValue {
key: String,
message: String,
},
#[error("Failed to parse configuration: {message}")]
ParseError { message: String },
#[error("Schema validation failed: {0}")]
SchemaValidation(String),
}
Creating ConfigError
fn validate_config(&self, config: &JsonValue) -> Result<(), ConfigError> {
// Key not found
let api_key = config.get("api_key")
.ok_or_else(|| ConfigError::NotFound("api_key".to_string()))?;
// Invalid value
let max_items = config.get("max_items")
.and_then(|v| v.as_i64())
.ok_or_else(|| ConfigError::InvalidValue {
key: "max_items".to_string(),
message: "must be an integer".to_string(),
})?;
if max_items > 10000 {
return Err(ConfigError::InvalidValue {
key: "max_items".to_string(),
message: "cannot exceed 10000".to_string(),
});
}
// Parse error
let url = config.get("api_url")
.and_then(|v| v.as_str())
.ok_or_else(|| ConfigError::ParseError {
message: "api_url must be a string".to_string(),
})?;
url::Url::parse(url).map_err(|e| ConfigError::ParseError {
message: e.to_string(),
})?;
Ok(())
}
Extension Error Handling
Extensions should define their own error types:
use thiserror::Error;
use axum::http::StatusCode;
use systemprompt::prelude::ExtensionError;
#[derive(Error, Debug)]
pub enum MyExtensionError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Validation failed: {0}")]
Validation(String),
#[error("External service error: {0}")]
External(String),
}
impl MyExtensionError {
pub fn code(&self) -> &'static str {
match self {
Self::NotFound(_) => "NOT_FOUND",
Self::Database(_) => "DATABASE_ERROR",
Self::Validation(_) => "VALIDATION_ERROR",
Self::External(_) => "EXTERNAL_ERROR",
}
}
pub fn status(&self) -> StatusCode {
match self {
Self::NotFound(_) => StatusCode::NOT_FOUND,
Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Validation(_) => StatusCode::BAD_REQUEST,
Self::External(_) => StatusCode::BAD_GATEWAY,
}
}
pub fn is_retryable(&self) -> bool {
matches!(self, Self::Database(_) | Self::External(_))
}
}
Error Context
Add context to errors:
use anyhow::Context;
async fn load_extension(id: &str) -> Result<Arc<dyn Extension>, LoaderError> {
let ext = registry.get(id)
.ok_or_else(|| LoaderError::MissingDependency {
extension: "current".to_string(),
dependency: id.to_string(),
})?;
ext.validate()
.context("validation failed")
.map_err(|e| LoaderError::InitializationFailed {
extension: id.to_string(),
message: e.to_string(),
})?;
Ok(ext.clone())
}
Debugging
Enable Debug Logging
RUST_LOG=systemprompt=debug systemprompt infra services serve
Common Error Messages
MissingDependency:
Extension 'oauth' requires dependency 'users' which is not registered
→ Ensure the 'users' extension is registered in the ExtensionBuilder chain before its dependents
DependencyCycle:
Dependency cycle detected while ordering extensions: a -> b -> c -> a
→ Restructure dependencies to break the cycle
ReservedPathCollision:
Extension 'my-ext' uses reserved API path '/api/v1/users'
→ Change to a non-reserved path like /api/v1/my-ext
SchemaInstallationFailed:
Failed to install schema for extension 'users': relation "users" already exists
→ Use CREATE TABLE IF NOT EXISTS
ConfigValidationFailed:
Configuration validation failed for extension 'my-ext': api_url must use HTTPS
→ Fix the value in the extension's YAML config under services/