Welcome to Chapter 15 of our journey to build a production-grade Rust static site generator! Up until now, we’ve focused on building out core functionalities like content parsing, templating, and routing. While our SSG can generate sites, it’s not yet resilient to real-world issues like malformed content files, missing templates, or unexpected I/O errors. In a production environment, an application that crashes silently or provides cryptic error messages is a nightmare to maintain.

This chapter is dedicated to transforming our SSG into a robust, diagnosable, and maintainable system. We will implement a comprehensive error handling strategy using Rust’s powerful thiserror and anyhow crates, providing clear and actionable error messages. Concurrently, we will integrate structured logging with the tracing ecosystem, allowing us to monitor the SSG’s operations, diagnose problems efficiently, and understand its internal state. By the end of this chapter, our SSG will not only work correctly but also gracefully handle failures and provide invaluable insights into its behavior, making it truly production-ready.

Planning & Design

A well-designed error handling and logging strategy is crucial for any application, especially one that processes user-generated content like an SSG. We need a system that can:

  1. Distinguish between different types of errors: Is it a file not found, a parsing error, or a rendering issue?
  2. Provide context: Where did the error occur? Which file or component was involved?
  3. Propagate errors gracefully: Avoid panics and allow the application to attempt recovery or shut down cleanly.
  4. Offer actionable information: Help the user or developer understand how to fix the problem.
  5. Log relevant events: Track the SSG’s lifecycle, content processing, and any warnings or errors that occur.

Error Architecture

We’ll adopt a two-tiered error handling approach:

  • thiserror for Library-Specific Errors: For errors originating within specific modules (e.g., parser, renderer, file_system), we’ll define custom error enums using thiserror. This allows us to create distinct, strongly typed errors that encapsulate specific failure modes and provide detailed context. It’s excellent for library-level errors where you want to expose specific error variants.
  • anyhow for Application-Level Errors: At the application’s top level (e.g., in main.rs or the build orchestration logic), we’ll use anyhow::Error. This crate provides a convenient, generic error type that can wrap any error that implements std::error::Error, making it perfect for handling diverse errors that bubble up from various parts of the application without needing to define a monolithic error enum for every possible failure.

Logging Strategy with tracing

We’ll use the tracing ecosystem, which is Rust’s modern, structured logging and diagnostics framework.

  • tracing: The core crate providing macros (info!, warn!, error!, debug!, trace!) and Span functionality.
  • tracing-subscriber: Used to configure how tracing events are processed and outputted (e.g., to console, file, or external systems).
  • Structured Logging: tracing allows us to attach key-value pairs to log messages and spans, providing rich context that is invaluable for debugging and analysis, especially when logs are consumed by tools like ELK stack or Splunk.

Debugging Considerations

  • RUST_LOG Environment Variable: tracing-subscriber respects the RUST_LOG environment variable, allowing dynamic control over log levels at runtime without recompiling.
  • Conditional Compilation: We can use cfg(debug_assertions) to include extra debugging code only in debug builds.
  • Tooling: Mentioning rust-analyzer for IDE integration, debugger support (e.g., with VS Code and lldb/gdb).

Architecture Flow for Error Handling and Logging

Let’s visualize how errors and logs will flow through our SSG pipeline.

flowchart TD Build_Process[SSG Build Process Start] --> Init_Logging[Initialize Tracing Logger] Init_Logging --> Config_Load[Load Configuration] Config_Load --->|ConfigError| Handle_Fatal_Error[Handle Fatal Error Exit] Config_Load --> Scan_Content[Scan Content Directory] Scan_Content --->|IoError| Log_Warn_Error[Log Warning or Continue Exit] Scan_Content --> Collect_Content[Collect Content Files] subgraph Content_Processing_Flow["Content Processing Flow Parallel"] direction LR Process_File[Process Content File] Process_File --> Read_File[Read File Content] Read_File --->|IoError| Content_File_Error[Handle Content File Error] Read_File --> Parse_Frontmatter[Parse Frontmatter] Parse_Frontmatter --->|FrontmatterError| Content_File_Error Parse_Frontmatter --> Parse_Markdown[Parse Markdown] Parse_Markdown --->|MarkdownError| Content_File_Error Parse_Markdown --> Render_Template[Render with Template] Render_Template --->|TemplateError| Content_File_Error Render_Template --> Write_Output[Write Output HTML] Write_Output --->|IoError| Content_File_Error Content_File_Error --> Log_Content_Error[Log Specific Content Error] end Collect_Content --> Process_File Log_Warn_Error --> Continue_Build[Continue Build if Non Fatal] Continue_Build --> Generate_Sitemap[Generate Sitemap and Feeds] Generate_Sitemap --->|SitemapError| Log_Warn_Error Generate_Sitemap --> Build_Complete[SSG Build Complete] Handle_Fatal_Error --> Exit_Failure[Exit with Failure Code] Log_Warn_Error --> Exit_Success_or_Failure[Exit with Success or Failure Code] Build_Complete --> Exit_Success[Exit with Success Code] style Content_File_Error fill:#f9f,stroke:#333,stroke-width:2px style Log_Warn_Error fill:#f9f,stroke:#333,stroke-width:2px style Handle_Fatal_Error fill:#f9f,stroke:#333,stroke-width:2px

Step-by-Step Implementation

a) Setup/Configuration

First, we need to add the necessary crates to our Cargo.toml.

File: Cargo.toml

# ... other dependencies

[dependencies]
# Error Handling
anyhow = "1.0.80"          # For application-level errors
thiserror = "1.0.58"       # For library-level specific errors

# Logging & Tracing
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"] } # For console output and RUST_LOG env var
# Optional: tracing-appender = "0.2.3" # For file logging, if needed

Next, we’ll initialize the tracing subscriber in our main.rs to ensure logs are captured and displayed.

File: src/main.rs

use anyhow::Result;
use tracing::{info, error, Level};
use tracing_subscriber::{EnvFilter, fmt};

// Assuming existing imports for your SSG logic
// ...

#[tokio::main] // If you're using tokio for async operations
async fn main() -> Result<()> {
    // 1. Initialize Tracing Subscriber
    // This sets up a subscriber that reads the RUST_LOG environment variable
    // for filtering and prints formatted logs to the console.
    // Default level is INFO if RUST_LOG is not set.
    fmt::Subscriber::builder()
        .with_env_filter(EnvFilter::from_default_env().add_directive(Level::INFO.into()))
        .init();

    info!("Starting SSG build process...");

    // Existing SSG build logic
    // let config = load_config()?; // Example of using '?' with anyhow::Result
    // ...

    info!("SSG build process completed successfully.");
    Ok(())
}

Explanation:

  • anyhow::Result is an alias for std::result::Result<T, anyhow::Error>, simplifying function signatures.
  • tracing provides the macros like info!, error!.
  • tracing_subscriber is configured to:
    • Use EnvFilter to allow filtering log messages based on the RUST_LOG environment variable (e.g., RUST_LOG=debug cargo run).
    • Set a default log level of INFO if RUST_LOG is not specified.
    • Use fmt to format the logs for console output.
    • init() registers this subscriber globally.

b) Core Implementation - Error Handling with thiserror and anyhow

Let’s define a central AppError enum using thiserror that will represent all possible custom errors within our SSG. We’ll place this in a new src/error.rs module.

File: src/error.rs

use std::path::PathBuf;
use thiserror::Error;

/// Defines all custom error types for our Static Site Generator.
#[derive(Error, Debug)]
pub enum AppError {
    /// Error related to file system operations (e.g., reading, writing, path issues).
    #[error("File system error: {0}")]
    Io(#[from] std::io::Error),

    /// Error related to configuration loading or parsing.
    #[error("Configuration error: {0}")]
    Config(String),

    /// Error related to parsing frontmatter (YAML/TOML).
    #[error("Frontmatter parsing error in '{path}': {source}")]
    Frontmatter {
        path: PathBuf,
        #[source]
        source: serde_yaml::Error, // Or `toml::de::Error` if you only use TOML
    },

    /// Error related to template rendering (e.g., Tera template errors).
    #[error("Template rendering error in '{template_name}': {source}")]
    Template {
        template_name: String,
        #[source]
        source: tera::Error,
    },

    /// Error related to parsing Markdown content.
    #[error("Markdown parsing error in '{path}': {message}")]
    Markdown {
        path: PathBuf,
        message: String,
    },

    /// Error for invalid or unexpected content structure.
    #[error("Invalid content structure: {0}")]
    InvalidContent(String),

    /// Error for duplicate routes detected during build.
    #[error("Duplicate route detected: '{route}' for content '{path}'")]
    DuplicateRoute {
        route: String,
        path: PathBuf,
    },

    /// A generic catch-all error for situations where a specific custom error isn't needed.
    #[error("An unexpected error occurred: {0}")]
    Other(String),
}

// Helper to convert anyhow::Error into AppError::Other for specific cases
impl From<anyhow::Error> for AppError {
    fn from(err: anyhow::Error) -> Self {
        AppError::Other(err.to_string())
    }
}

Explanation:

  • #[derive(Error, Debug)] automatically implements the Error and Debug traits.
  • #[error("...")] defines the display message for each error variant.
  • #[from] std::io::Error allows std::io::Error to be automatically converted into AppError::Io using the ? operator. This is a common pattern for integrating system errors.
  • #[source] indicates the underlying cause of the error, which is useful for error reporting chains.
  • We include specific fields like path, template_name, message to provide rich context.
  • The From<anyhow::Error> for AppError implementation is a convenience for when you might have an anyhow::Error that needs to be wrapped into your specific AppError type, though often anyhow::Error will be used at the top level.

Now, let’s update some existing functions to use AppError and anyhow::Result.

First, add mod error; to src/main.rs and pub use error::AppError; to src/lib.rs (if you have one) or directly import AppError where needed.

File: src/main.rs (Modify main function)

use anyhow::Result; // Keep using anyhow::Result for top-level
use tracing::{info, error, Level};
use tracing_subscriber::{EnvFilter, fmt};

// ... existing imports
mod error; // Add this line
use crate::error::AppError; // Import our custom error type

#[tokio::main]
async fn main() -> Result<()> { // Main now returns anyhow::Result<()>
    fmt::Subscriber::builder()
        .with_env_filter(EnvFilter::from_default_env().add_directive(Level::INFO.into()))
        .init();

    info!("Starting SSG build process...");

    // Example: Replace a placeholder build function with error handling
    if let Err(e) = run_build_process().await { // Call a function that returns AppError
        error!("SSG build failed: {:?}", e);
        // If it's a specific AppError variant, you could handle it differently
        // For example, if e is AppError::Config, suggest checking config file.
        // We propagate it as anyhow::Error for main's return type.
        return Err(e.into()); // Convert AppError to anyhow::Error
    }

    info!("SSG build process completed successfully.");
    Ok(())
}

/// A placeholder for our main build orchestration logic.
/// This function will now return our custom AppError.
async fn run_build_process() -> std::result::Result<(), AppError> {
    // Simulate configuration loading
    info!("Loading configuration...");
    let config_path = PathBuf::from("config.toml");
    if !config_path.exists() {
        return Err(AppError::Config(format!("Configuration file not found at {:?}", config_path)));
    }
    // In a real scenario, deserialize config and handle errors
    // let config_content = std::fs::read_to_string(&config_path)?; // This will convert io::Error to AppError::Io
    info!("Configuration loaded successfully.");

    // Simulate content scanning
    info!("Scanning content directory...");
    // If a directory doesn't exist, this would trigger an Io error
    // std::fs::read_dir("non_existent_dir")?; // Example of how Io error would propagate

    // Simulate content parsing and template rendering
    // For now, let's simulate a frontmatter error
    let dummy_content_path = PathBuf::from("content/posts/malformed.md");
    if dummy_content_path.file_name().unwrap_or_default() == "malformed.md" {
        warn!("Simulating a frontmatter error for '{}'", dummy_content_path.display());
        return Err(AppError::Frontmatter {
            path: dummy_content_path,
            source: serde_yaml::from_str::<serde_yaml::Value>("invalid yaml: -").unwrap_err(), // Create a dummy error
        });
    }

    // Simulate a template error
    let dummy_template_name = "non_existent_template.html".to_string();
    if dummy_template_name == "non_existent_template.html" {
        warn!("Simulating a template error for '{}'", dummy_template_name);
        return Err(AppError::Template {
            template_name: dummy_template_name,
            source: tera::Error::msg("Template not found"), // Create a dummy Tera error
        });
    }

    // ... continue with actual build logic
    Ok(())
}

Explanation:

  • main now uses anyhow::Result<()>. This allows it to accept anyhow::Error or any type that can be converted into it (like our AppError).
  • run_build_process returns std::result::Result<(), AppError>, meaning it will produce our specific AppError variants.
  • The ? operator is used to propagate errors. When run_build_process returns an AppError, main catches it, logs it, and then converts it into an anyhow::Error using e.into() before returning.
  • We’ve added placeholder error simulations to demonstrate how different AppError variants would be returned.

Let’s integrate this into a more concrete part of our SSG: the content parsing module. Assuming you have a src/parser.rs module.

File: src/parser.rs (Example modification)

use std::{fs, path::Path};
use serde::Deserialize;
use pulldown_cmark::{Parser, Options, html};
use crate::error::AppError; // Import AppError
use tracing::{info, debug, instrument}; // Import tracing macros

/// Represents the parsed content, including frontmatter and HTML body.
pub struct ParsedContent {
    pub frontmatter: Frontmatter,
    pub html_body: String,
}

/// Example Frontmatter structure.
#[derive(Debug, Deserialize)]
pub struct Frontmatter {
    pub title: String,
    pub date: Option<String>,
    // ... other frontmatter fields
}

/// Parses a content file, extracting frontmatter and converting Markdown to HTML.
#[instrument(skip(file_path), fields(file = %file_path.display()))] // Add tracing span for this function
pub fn parse_content_file(file_path: &Path) -> Result<ParsedContent, AppError> {
    debug!("Parsing content file: {:?}", file_path);

    let content = fs::read_to_string(file_path)
        .map_err(|e| AppError::Io(e))?; // Convert std::io::Error to AppError::Io

    let parts: Vec<&str> = content.split("---").collect();

    if parts.len() < 3 {
        return Err(AppError::InvalidContent(format!("Missing frontmatter in {:?}", file_path)));
    }

    let frontmatter_str = parts[1].trim();
    let markdown_str = parts[2..].join("---").trim(); // Join remaining parts in case markdown contains "---"

    let frontmatter: Frontmatter = serde_yaml::from_str(frontmatter_str)
        .map_err(|e| AppError::Frontmatter {
            path: file_path.to_path_buf(),
            source: e,
        })?;

    info!("Successfully parsed frontmatter for '{:?}'", file_path);
    debug!("Frontmatter: {:?}", frontmatter);

    let mut options = Options::empty();
    options.insert(Options::ENABLE_TABLES);
    options.insert(Options::ENABLE_FOOTNOTES);
    options.insert(Options::ENABLE_TASKLISTS);
    options.insert(Options::ENABLE_STRIKETHROUGH);

    let parser = Parser::new_ext(markdown_str, options);
    let mut html_output = String::new();
    html::push_html(&mut html_output, parser);

    debug!("Converted Markdown to HTML for '{:?}'", file_path);

    Ok(ParsedContent {
        frontmatter,
        html_body: html_output,
    })
}

Explanation:

  • use crate::error::AppError; brings our custom error types into scope.
  • fs::read_to_string(file_path).map_err(|e| AppError::Io(e))? demonstrates converting a std::io::Error into our AppError::Io variant.
  • serde_yaml::from_str(...).map_err(|e| AppError::Frontmatter { ... })? shows how to create a specific AppError::Frontmatter with context (path and source error).
  • AppError::InvalidContent is used for structural issues like missing frontmatter.
  • #[instrument] macro from tracing automatically creates a tracing::Span for the function, making it easier to see function execution in logs, and fields(file = %file_path.display()) adds contextual data to the span.

c) Core Implementation - Logging with tracing

We’ve already set up the tracing-subscriber in main.rs. Now, let’s sprinkle tracing macros throughout our codebase.

File: src/main.rs (Already shown, but highlighting log calls)

// ...
info!("Starting SSG build process...");

if let Err(e) = run_build_process().await {
    error!("SSG build failed: {:?}", e); // Logs the error
    return Err(e.into());
}

info!("SSG build process completed successfully.");
// ...

File: src/parser.rs (Already shown, but highlighting log calls)

use tracing::{info, debug, instrument}; // Import tracing macros

// ... parse_content_file function
#[instrument(skip(file_path), fields(file = %file_path.display()))]
pub fn parse_content_file(file_path: &Path) -> Result<ParsedContent, AppError> {
    debug!("Parsing content file: {:?}", file_path); // Debug-level log

    // ... error handling ...

    info!("Successfully parsed frontmatter for '{:?}'", file_path); // Info-level log
    debug!("Frontmatter: {:?}", frontmatter); // Debug-level log of data

    // ... markdown parsing ...

    debug!("Converted Markdown to HTML for '{:?}'", file_path); // Debug-level log

    Ok(ParsedContent { /* ... */ })
}

You should integrate info!, warn!, error!, debug!, trace! macros in other parts of your SSG as well.

Example: src/renderer.rs (Illustrative, assuming you have one)

use tera::{Tera, Context};
use std::path::PathBuf;
use crate::error::AppError;
use tracing::{info, debug, warn, error, instrument};

pub struct SiteRenderer {
    tera: Tera,
    // ... other fields
}

impl SiteRenderer {
    pub fn new(template_dir: &Path) -> Result<Self, AppError> {
        info!("Initializing Tera templates from: {:?}", template_dir);
        let mut tera = Tera::new(&format!("{}/**/*.html", template_dir.display()))
            .map_err(|e| AppError::Template {
                template_name: "initialization".to_string(), // Or a more specific name
                source: e,
            })?;
        tera.autoescape_on(vec![".html"]); // Best practice for security
        info!("Tera templates initialized successfully.");
        Ok(SiteRenderer { tera })
    }

    #[instrument(skip(self, context), fields(template = %template_name))]
    pub fn render_page(&self, template_name: &str, context: &Context) -> Result<String, AppError> {
        debug!("Attempting to render template: '{}'", template_name);
        self.tera.render(template_name, context)
            .map_err(|e| AppError::Template {
                template_name: template_name.to_string(),
                source: e,
            })
    }

    pub fn write_output(&self, output_path: &PathBuf, content: &str) -> Result<(), AppError> {
        info!("Writing output to: {:?}", output_path);
        // Ensure parent directories exist
        if let Some(parent) = output_path.parent() {
            if !parent.exists() {
                debug!("Creating parent directory: {:?}", parent);
                std::fs::create_dir_all(parent)?; // Automatically converts to AppError::Io
            }
        }
        std::fs::write(output_path, content)?; // Automatically converts to AppError::Io
        info!("Successfully wrote output to: {:?}", output_path);
        Ok(())
    }
}

Explanation:

  • SiteRenderer::new now returns Result<Self, AppError> and handles tera::Error during initialization.
  • render_page and write_output also return Result and use ? for error propagation.
  • #[instrument] is used on render_page to automatically create a span and add the template name as a field.
  • info!, debug!, warn!, error! are used at appropriate points to provide visibility into the rendering process. tera.autoescape_on is a security best practice to prevent XSS.

d) Debugging Enhancements

Using RUST_LOG: To control the verbosity of your logs, set the RUST_LOG environment variable.

  • RUST_LOG=info cargo run: Shows INFO and higher (WARN, ERROR) messages.
  • RUST_LOG=debug cargo run: Shows DEBUG and higher messages.
  • RUST_LOG=trace cargo run: Shows all messages, including TRACE.
  • RUST_LOG=my_ssg_crate=debug cargo run: If your crate is named my_ssg_crate, this only shows DEBUG messages for your code.
  • RUST_LOG=my_ssg_crate=debug,tera=info cargo run: Shows debug for your crate, but only info for the tera crate.

This dynamic control is incredibly powerful for debugging specific issues without recompiling.

Temporary eprintln!: For quick, temporary debugging, especially when you suspect a variable’s value at a specific point, eprintln! can be useful.

fn some_function(value: &str) {
    eprintln!("DEBUG: value received: {}", value); // Prints to stderr immediately
    // ...
}

Caution: Remove these before committing to production. tracing::debug! is the preferred way to include debug information that can be toggled via RUST_LOG.

Conditional Compilation for Debug-Only Code: You can wrap debug-specific logic in #[cfg(debug_assertions)] blocks. This code will only be included when compiling in debug mode (cargo build without --release).

#[cfg(debug_assertions)]
fn print_expensive_debug_info() {
    println!("This expensive debug info is only compiled in debug builds!");
}

fn main() {
    // ...
    #[cfg(debug_assertions)]
    print_expensive_debug_info();
    // ...
}

Production Considerations

  1. Graceful Degradation & User Feedback:

    • Fatal Errors: For errors that prevent the SSG from building anything (e.g., config file not found), the process should exit with a non-zero status code and print a clear error message to stderr.
    • Content-Specific Errors: If only one content file or template fails, the SSG should ideally log the error, skip that specific problematic file, and continue building the rest of the site. This prevents a single bad file from taking down the entire build.
    • Error Pages: Consider generating a simple “build failed” HTML page if the entire build process crashes or a specific content item cannot be rendered, to provide some output rather than nothing.
  2. Performance & Logging Overhead:

    • Log Levels: In production, set RUST_LOG=info or RUST_LOG=warn to minimize logging overhead. debug and trace levels generate a lot of data and can impact performance.
    • Asynchronous Logging: For very high-throughput applications, writing logs synchronously can block the main thread. tracing-appender (mentioned in Cargo.toml comments) can be used to send logs to a separate thread or process, reducing performance impact. For an SSG, this is usually not strictly necessary unless you’re processing hundreds of thousands of files very frequently.
  3. Security:

    • Sensitive Information: Never log sensitive data (API keys, user passwords, personal identifiable information). Redact or mask such information before it hits the logs.
    • Log Injection: Ensure that user-provided input, when logged, is properly sanitized to prevent log injection attacks (though less common in SSGs, it’s a good habit).
    • Error Message Detail: Avoid exposing excessive internal details in error messages that might be visible to end-users on publicly deployed sites. Error messages should be informative for developers but not leak system architecture or vulnerabilities.
  4. Monitoring & Alerts:

    • Structured Logs: tracing’s structured logging makes it easier to export logs to centralized logging systems (e.g., Elastic Stack, Splunk, Loki). These systems can parse the key-value pairs, allowing for powerful querying, analysis, and dashboarding.
    • Alerting: Configure monitoring systems to trigger alerts (e.g., email, Slack notifications) if a high volume of error! or warn! messages are detected during a build, indicating potential issues with content or templates.

Code Review Checkpoint

At this stage, we have significantly improved the robustness and diagnosability of our SSG.

Summary of what was built:

  • Introduced thiserror for defining custom, strongly-typed error variants for specific failures within our SSG modules.
  • Integrated anyhow for simplified application-level error handling, allowing main to gracefully catch and report diverse errors.
  • Set up the tracing ecosystem for structured logging, enabling us to output informative messages at different verbosity levels.
  • Demonstrated how to use tracing::info!, debug!, warn!, error! macros and #[instrument] to add context to logs.
  • Refactored src/main.rs, src/error.rs, src/parser.rs, and src/renderer.rs (example) to incorporate the new error handling and logging patterns.

Files created/modified:

  • Cargo.toml: Added anyhow, thiserror, tracing, tracing-subscriber.
  • src/error.rs: New file defining AppError enum.
  • src/main.rs: Initialized tracing-subscriber, imported AppError, modified main to use anyhow::Result and call a build function returning AppError.
  • src/parser.rs: Modified parse_content_file to return Result<ParsedContent, AppError> and use tracing macros.
  • src/renderer.rs (example): Modified SiteRenderer methods to return Result<T, AppError> and use tracing macros.

How it integrates with existing code: The error handling and logging mechanisms are now woven into the core pipeline. Functions that previously returned simple Result<T, E> (where E might have been std::io::Error or a generic string) now return Result<T, AppError>, providing much richer error context. Logging calls are integrated at key points to track execution flow and report events.

Common Issues & Solutions

  1. Issue: “The ? operator can only be used in a function that returns Result or Option (or another type that implements Try)”.

    • Problem: You’re trying to use ? in a function that doesn’t declare a Result return type, or the error type doesn’t match.
    • Solution: Ensure your function signature is fn my_func() -> Result<T, AppError> (or anyhow::Result<T>). Also, if you use ? on a Result<T, E_other>, ensure E_other can be converted into your function’s return error type (AppError in our case). If E_other is std::io::Error, #[from] std::io::Error in AppError handles this automatically. If not, you might need map_err as shown with serde_yaml::Error.
  2. Issue: “Logs are not appearing in the console”, or “Only error! messages are showing”.

    • Problem: The tracing-subscriber might not be initialized, or the RUST_LOG environment variable is set too restrictively.
    • Solution:
      • Verify fmt::Subscriber::builder().with_env_filter(...).init(); is called exactly once at the very beginning of main.
      • Check your RUST_LOG environment variable. If it’s RUST_LOG=error, only error messages will show. Try RUST_LOG=info or RUST_LOG=debug to see more.
      • Ensure you have tracing-subscriber with env-filter and fmt features enabled in Cargo.toml.
  3. Issue: “My custom error variant requires a PathBuf, but I only have a &Path.”

    • Problem: Path is a borrowed type, PathBuf is owned. Error types often need to own their data.
    • Solution: Use file_path.to_path_buf() to create an owned PathBuf from a borrowed &Path.

Testing & Verification

To verify our error handling and logging, we need to intentionally introduce errors and observe the SSG’s behavior and its output.

  1. Trigger Io Error:

    • Modify src/main.rs to attempt to load a config.toml from a non-existent path.
    • Expected Behavior: The SSG should print an error! message indicating “File system error: No such file or directory…” and exit with a non-zero status code.
  2. Trigger Frontmatter Error:

    • Create a file content/posts/invalid-frontmatter.md:
      ---
      title: My Post
      date: 2026-03-02
      tags: [tag1, tag2
      ---
      # This is a post with invalid frontmatter
      
    • Expected Behavior: The SSG should log an error! message similar to “Frontmatter parsing error in ‘content/posts/invalid-frontmatter.md’: while parsing a block collection, did not find expected ‘-’ or a line break at line 4 column 1”. The build might continue if other files are valid, or fail if your build logic is set to be strict.
  3. Trigger Template Error:

    • Modify a content file’s frontmatter to specify a non-existent template, e.g., template: non_existent.html.
    • Expected Behavior: An error! message like “Template rendering error in ’non_existent.html’: Template not found” should appear.
  4. Verify Log Levels:

    • Run cargo run (default INFO level). You should see info! and warn!/error! messages.
    • Run RUST_LOG=debug cargo run. You should now see all debug! messages appearing, providing much more detail about file parsing and rendering.
    • Run RUST_LOG=error cargo run. Only error! messages should be visible.

By systematically introducing these errors and verifying the output, you can confirm that your error handling and logging mechanisms are working as expected.

Summary & Next Steps

In this chapter, we’ve significantly enhanced the robustness and diagnosability of our Rust static site generator. We established a comprehensive error handling strategy using thiserror for domain-specific errors and anyhow for top-level application errors, ensuring that our SSG can gracefully handle and report various failure conditions. Furthermore, we integrated the tracing ecosystem, enabling structured logging that provides invaluable insights into the SSG’s internal operations, crucial for debugging and monitoring in production. These additions make our SSG far more resilient, maintainable, and ready for real-world deployment.

With a solid foundation in error handling and logging, we can now confidently build more complex and performance-critical features. In the next chapter, Chapter 16: Incremental Builds, Caching, and Content Diffing, we will tackle performance by implementing sophisticated mechanisms to detect changes, cache build artifacts, and only rebuild what’s necessary, drastically speeding up the development feedback loop and build times for large sites.