Skip to main content
Background Image
  1. Blog/

Premortem vs Figment: Configuration Libraries for Rust Applications

Author
Glen Baker
Building open source tooling
Table of Contents

The Problem: Death by a Thousand Configuration Errors
#

You deploy your application, and it crashes. Missing database host. You fix it, redeploy. Invalid port value. Fix, redeploy. Pool size must be positive. Fix, redeploy.

$ ./myapp
Error: missing field `database.host`

$ ./myapp  # fixed it, try again
Error: invalid port value

$ ./myapp  # fixed that too
Error: pool_size must be positive

# Three deaths to find three problems

This is the postmortem experience - discovering problems one at a time, after they’ve already killed your application.

What if you could perform a premortem instead? Find all the ways your configuration would fail before the application ever runs?

$ ./myapp
Configuration errors (3):
  [config.toml:8] missing required field 'database.host'
  [env:APP_PORT] value "abc" is not a valid integer
  [config.toml:10] 'pool_size' value -5 must be >= 1

One run. All errors. Know how your app would die before it does.

Two Philosophies
#

Premortem and Figment are both excellent Rust configuration libraries, but they’re designed with fundamentally different priorities.

AspectPremortemFigment
PhilosophyFind ALL config problems before runtimeFlexible configuration with profile support
Error HandlingGuaranteed error accumulationSerde-based (fail-fast deserialization)
ValidationBuilt-in validators + custom Validate traitVia serde deserializers
TestingVirtual filesystem (MockEnv)Real temp directories (Jail)
EcosystemStandalone, uses stillwater for FP patternsPowers the Rocket framework

Error Handling: The Core Difference
#

This is where the libraries diverge most significantly.

Figment: Fail-Fast with Provider Composition
#

Figment uses serde’s deserialization, which stops at the first error:

use figment::{Figment, providers::{Toml, Env}};

let figment = Figment::new()
    .merge(Toml::file("config.toml"))
    .merge(Env::prefixed("APP_"));

match figment.extract::<Config>() {
    Ok(config) => { /* use config */ }
    Err(e) => {
        // Can iterate, but serde stops at first error
        for error in e {
            println!("Error: {}", error);
        }
    }
}

When your TOML file has three problems, you’ll discover them one deployment at a time.

Premortem: Guaranteed Error Accumulation
#

Premortem uses stillwater’s Validation type to guarantee ALL errors are collected:

use premortem::{Config, Validate, ConfigValidation};
use stillwater::Validation;

#[derive(Debug, Deserialize)]
struct DatabaseConfig {
    host: String,
    port: u16,
    pool_size: u32,
}

impl Validate for DatabaseConfig {
    fn validate(&self) -> ConfigValidation<()> {
        // ALL validations run, ALL errors collected
        Validation::all((
            validate_field(&self.host, "host", &[&NonEmpty]),
            validate_field(&self.port, "port", &[&Range(1..=65535)]),
            validate_field(&self.pool_size, "pool_size", &[&Range(1..=100)]),
        ))
        .map(|_| ())
    }
}

// Result: If host is empty, port is 0, and pool_size is 200,
// you get 3 errors in one response, not 3 separate runs

The Validation::all combinator runs every check regardless of earlier failures, collecting all errors into a single response.

Built-in Validation
#

Premortem: Extensive Validator Library
#

Premortem ships with validators for common scenarios:

use premortem::validate::validators::*;

// String validators
NonEmpty              // value.is_empty() == false
MinLength(n)          // value.len() >= n
MaxLength(n)          // value.len() <= n
Pattern(regex)        // regex.is_match(value)
Email                 // RFC 5322-like validation
Url                   // URL format validation

// Numeric validators
Range(range)          // value in range
Positive              // value > 0
NonZero               // value != 0

// Collection validators
NonEmptyCollection    // !collection.is_empty()
MinItems(n)           // collection.len() >= n
Each(validator)       // validate each item

// Path validators
FileExists            // path.is_file()
DirExists             // path.is_dir()
ParentExists          // path.parent().is_dir()

Figment: Via Serde Deserializers
#

Figment relies on custom serde deserializers for validation:

#[derive(Deserialize)]
struct Config {
    #[serde(deserialize_with = "validate_port")]
    port: u16,
}

fn validate_port<'de, D>(deserializer: D) -> Result<u16, D::Error>
where
    D: Deserializer<'de>,
{
    let port = u16::deserialize(deserializer)?;
    if port == 0 {
        return Err(serde::de::Error::custom("port cannot be 0"));
    }
    Ok(port)
}

This works, but requires more boilerplate and doesn’t accumulate errors across fields.

Cross-Field Validation
#

Real-world configuration often has fields that depend on each other.

Premortem: First-Class Support
#

impl Validate for DateRange {
    fn validate(&self) -> ConfigValidation<()> {
        let field_validation = Validation::all((
            validate_field(&self.start_date, "start_date", &[&NonEmpty]),
            validate_field(&self.end_date, "end_date", &[&NonEmpty]),
        ));

        let cross_field = if self.start_date > self.end_date {
            Validation::Failure(ConfigErrors::single(ConfigError::CrossFieldError {
                fields: vec!["start_date".into(), "end_date".into()],
                message: "start_date must be before end_date".into(),
                source_location: None,
            }))
        } else {
            Validation::Success(())
        };

        field_validation.and(cross_field).map(|_| ())
    }
}

Figment: Post-Extraction Validation
#

With Figment, cross-field validation happens after extraction:

let config: Config = figment.extract()?;

// Manual validation after the fact
if config.start_date > config.end_date {
    return Err(anyhow!("start_date must be before end_date"));
}

This means cross-field errors are discovered separately from field-level errors.

Profile Support
#

Figment: First-Class Profiles
#

Figment excels at profile-based configuration:

use figment::{Figment, Profile};
use figment::providers::{Toml, Env, Serialized};

let figment = Figment::new()
    .merge(Serialized::defaults(Config::default()))
    .merge(Toml::file("config.toml").nested())  // Top-level keys are profiles
    .merge(Env::prefixed("APP_"))
    .select(Profile::from_env_or("APP_PROFILE", "development"));
# config.toml
[default]
port = 8080

[development]
debug = true

[production]
debug = false

Built-in profiles like Profile::Default and Profile::Global provide powerful inheritance patterns.

Premortem: Manual Profile Handling
#

Premortem doesn’t have built-in profiles, but they’re easy to implement:

let profile = std::env::var("APP_PROFILE").unwrap_or("development".into());

let config = Config::<AppConfig>::builder()
    .source(Defaults::from(AppConfig::default()))
    .source(Toml::file("config.toml"))
    .source(Toml::file(format!("config.{}.toml", profile)))
    .source(Env::prefix("APP_"))
    .build()?;

Merge Strategies
#

Figment: Four Strategies
#

Figment offers fine-grained control over how sources combine:

StrategyBehaviorArrays
mergeLater values winReplace
joinEarlier values winReplace
admergeLater values winConcatenate
adjoinEarlier values winConcatenate
let figment = Figment::new()
    .join(Serialized::defaults(Config::default()))  // Lower priority
    .merge(Toml::file("config.toml"))               // Higher priority
    .admerge(Env::prefixed("APP_"));                // Env + array concat

Premortem: Simple Layered Override
#

Premortem uses straightforward layered overriding - later sources win:

let config = Config::<AppConfig>::builder()
    .source(Defaults::from(AppConfig::default()))  // Base
    .source(Toml::file("config.toml"))             // Override defaults
    .source(Env::prefix("APP_"))                   // Override file
    .build()?;

Less flexible, but also less cognitive overhead.

Testing
#

Both libraries prioritize testability, but take different approaches.

Premortem: Virtual Filesystem
#

Premortem uses dependency injection with a MockEnv:

use premortem::{Config, MockEnv};

#[test]
fn test_config_loading() {
    let env = MockEnv::new()
        .with_file("config.toml", r#"
            host = "localhost"
            port = 8080
        "#)
        .with_env("APP_HOST", "testhost");

    let config = Config::<AppConfig>::builder()
        .source(Toml::file("config.toml"))
        .source(Env::prefix("APP_"))
        .build_with_env(&env)
        .unwrap();

    assert_eq!(config.host, "testhost");
}

#[test]
fn test_file_not_found() {
    let env = MockEnv::new()
        .with_missing_file("config.toml");

    let result = Config::<AppConfig>::builder()
        .source(Toml::file("config.toml"))
        .build_with_env(&env);

    assert!(matches!(
        result.unwrap_err().first(),
        ConfigError::SourceError { kind: SourceErrorKind::NotFound { .. }, .. }
    ));
}

No actual files created. Fast tests. Easy to simulate edge cases like permission errors.

Figment: Real Temp Directories
#

Figment’s Jail creates actual temporary files:

use figment::{Figment, Jail};

#[test]
fn test_config_loading() {
    Jail::expect_with(|jail| {
        jail.create_file("config.toml", r#"
            host = "localhost"
            port = 8080
        "#)?;

        jail.set_env("APP_HOST", "testhost");

        let config: Config = Figment::new()
            .merge(Toml::file("config.toml"))
            .merge(Env::prefixed("APP_"))
            .extract()?;

        assert_eq!(config.host, "testhost");
        Ok(())
    });
}

Real filesystem operations, automatically cleaned up.

Advanced Features
#

Premortem: Value Tracing
#

Debug where each value came from:

let traced = Config::<AppConfig>::builder()
    .source(Defaults::from(defaults))
    .source(Toml::file("config.toml"))
    .source(Env::prefix("APP_"))
    .build_traced()?;

// Check where a value came from
if let Some(trace) = traced.trace("database.host") {
    println!("Final value: {:?}", trace.final_value);
    println!("Source: {}", trace.final_value.source);

    // See override history
    for entry in &trace.history {
        println!("  {} -> {:?}", entry.source, entry.value);
    }
}

// Generate debug report
println!("{}", traced.trace_report());

Premortem: Hot Reload
#

#[cfg(feature = "watch")]
let (config, watcher) = Config::<AppConfig>::builder()
    .source(Toml::file("config.toml"))
    .source(Env::prefix("APP_"))
    .build_watched()?;

watcher.on_change(|event| {
    match event {
        ConfigEvent::Reloaded { old, new } => {
            println!("Config reloaded!");
        }
        ConfigEvent::ReloadFailed { errors } => {
            eprintln!("Reload failed: {:?}", errors);
        }
    }
});

Figment: Magic Values
#

Context-aware types that know where they came from:

use figment::value::magic::RelativePathBuf;

#[derive(Deserialize)]
struct Config {
    // Resolved relative to the config file's directory
    cert_path: RelativePathBuf,
}

// If config.toml is at /etc/app/config.toml and contains:
// cert_path = "certs/server.pem"
//
// The resolved path will be /etc/app/certs/server.pem

Feature Comparison Summary
#

FeaturePremortemFigment
Error accumulation (all errors)YesPartial
Built-in validatorsExtensiveVia serde
Custom validation traitValidateDeserializers
Cross-field validationYesManual
ProfilesManualFirst-class
Merge strategiesSimple override4 strategies
Array concatenationNoad- strategies
Value tracingbuild_traced()Metadata only
Hot reloadbuild_watched()No
Dependency injectionConfigEnvNo
Mock testingMockEnvJail
Magic valuesNoRelativePathBuf
TOML/JSON/YAMLYesYes

When to Choose Each
#

Choose Premortem If:
#

  • Complete error reporting is critical - You need ALL configuration errors upfront, not one at a time
  • Rich validation rules - You need built-in validators for strings, numbers, paths, collections
  • Cross-field validation - You need to validate relationships between fields
  • Functional programming style - You prefer stillwater’s Validation type
  • Testable I/O - You want virtual filesystem for fast unit tests
  • Hot reload - You need runtime configuration updates with change notifications
  • Debug tracing - You need to know exactly where each value came from and its override history

Choose Figment If:
#

  • Profile support - You need built-in dev/staging/production profiles
  • Flexible merging - You need fine-grained control over how sources combine
  • Array concatenation - You need to merge arrays from multiple sources
  • Rocket integration - You’re already using the Rocket web framework
  • Magic values - You need context-aware types like RelativePathBuf
  • Composable providers - You want to build complex provider hierarchies
  • Established ecosystem - You prefer a more mature, widely-used library

Basic Usage Comparison
#

Premortem
#

use premortem::{Config, Validate};
use premortem::sources::{Toml, Env, Defaults};

#[derive(Debug, Deserialize, Validate)]
struct AppConfig {
    #[validate(non_empty)]
    host: String,
    #[validate(range(1..=65535))]
    port: u16,
}

fn main() -> Result<(), ConfigErrors> {
    let config = Config::<AppConfig>::builder()
        .source(Defaults::from(AppConfig::default()))
        .source(Toml::file("config.toml"))
        .source(Env::prefix("APP_"))
        .build()?;

    println!("Server: {}:{}", config.host, config.port);
    Ok(())
}

Figment
#

use figment::{Figment, providers::{Format, Toml, Env, Serialized}};
use serde::Deserialize;

#[derive(Debug, Deserialize, Default)]
struct AppConfig {
    host: String,
    port: u16,
}

fn main() -> Result<(), figment::Error> {
    let config: AppConfig = Figment::new()
        .merge(Serialized::defaults(AppConfig::default()))
        .merge(Toml::file("config.toml"))
        .merge(Env::prefixed("APP_"))
        .extract()?;

    println!("Server: {}:{}", config.host, config.port);
    Ok(())
}

The Bottom Line
#

Both libraries are well-designed and production-ready. The choice comes down to your priorities:

If configuration errors in production have caused you pain - multiple deployments to fix multiple problems, unclear error messages, difficulty debugging where values came from - premortem is designed specifically to solve those problems.

If you need sophisticated profile handling, flexible merge strategies, or are already in the Rocket ecosystem - figment is mature, well-documented, and battle-tested.

You can even use both: figment for its profile and merge capabilities, with premortem’s validation layer on top.


Try Premortem
#

cargo add premortem
[dependencies]
premortem = { version = "0.3", features = ["toml", "json"] }

Run the error demo to see error accumulation in action:

git clone https://github.com/iepathos/premortem
cd premortem
cargo run --example error-demo

Documentation: docs.rs/premortem Repository: github.com/iepathos/premortem


Premortem is open source under the MIT license. Know how your app will die before it does.

Related

Premortem - Configuration Validation for Rust
Three Patterns That Made Prodigy's Functional Migration Worth It
Stillwater - Pure Core, Imperative Shell for Rust
Automating Documentation Maintenance with Prodigy: A Real-World Case Study
Debtmap - Rust Technical Debt Analyzer
Mermaid-Sonar: Detecting Hidden Complexity in Diagram Documentation
Transforming ripgrep's Documentation with AI Automation and MkDocs
Prodigy - AI Workflow Orchestration for Claude