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:
- Distinguish between different types of errors: Is it a file not found, a parsing error, or a rendering issue?
- Provide context: Where did the error occur? Which file or component was involved?
- Propagate errors gracefully: Avoid panics and allow the application to attempt recovery or shut down cleanly.
- Offer actionable information: Help the user or developer understand how to fix the problem.
- 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:
thiserrorfor Library-Specific Errors: For errors originating within specific modules (e.g.,parser,renderer,file_system), we’ll define custom error enums usingthiserror. 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.anyhowfor Application-Level Errors: At the application’s top level (e.g., inmain.rsor the build orchestration logic), we’ll useanyhow::Error. This crate provides a convenient, generic error type that can wrap any error that implementsstd::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!) andSpanfunctionality.tracing-subscriber: Used to configure howtracingevents are processed and outputted (e.g., to console, file, or external systems).- Structured Logging:
tracingallows 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_LOGEnvironment Variable:tracing-subscriberrespects theRUST_LOGenvironment 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-analyzerfor IDE integration, debugger support (e.g., with VS Code andlldb/gdb).
Architecture Flow for Error Handling and Logging
Let’s visualize how errors and logs will flow through our SSG pipeline.
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::Resultis an alias forstd::result::Result<T, anyhow::Error>, simplifying function signatures.tracingprovides the macros likeinfo!,error!.tracing_subscriberis configured to:- Use
EnvFilterto allow filtering log messages based on theRUST_LOGenvironment variable (e.g.,RUST_LOG=debug cargo run). - Set a default log level of
INFOifRUST_LOGis not specified. - Use
fmtto format the logs for console output. init()registers this subscriber globally.
- Use
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 theErrorandDebugtraits.#[error("...")]defines the display message for each error variant.#[from] std::io::Errorallowsstd::io::Errorto be automatically converted intoAppError::Iousing 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,messageto provide rich context. - The
From<anyhow::Error> for AppErrorimplementation is a convenience for when you might have ananyhow::Errorthat needs to be wrapped into your specificAppErrortype, though oftenanyhow::Errorwill 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:
mainnow usesanyhow::Result<()>. This allows it to acceptanyhow::Erroror any type that can be converted into it (like ourAppError).run_build_processreturnsstd::result::Result<(), AppError>, meaning it will produce our specificAppErrorvariants.- The
?operator is used to propagate errors. Whenrun_build_processreturns anAppError,maincatches it, logs it, and then converts it into ananyhow::Errorusinge.into()before returning. - We’ve added placeholder error simulations to demonstrate how different
AppErrorvariants 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 astd::io::Errorinto ourAppError::Iovariant.serde_yaml::from_str(...).map_err(|e| AppError::Frontmatter { ... })?shows how to create a specificAppError::Frontmatterwith context (path and source error).AppError::InvalidContentis used for structural issues like missing frontmatter.#[instrument]macro fromtracingautomatically creates atracing::Spanfor the function, making it easier to see function execution in logs, andfields(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::newnow returnsResult<Self, AppError>and handlestera::Errorduring initialization.render_pageandwrite_outputalso returnResultand use?for error propagation.#[instrument]is used onrender_pageto 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_onis 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: ShowsINFOand higher (WARN, ERROR) messages.RUST_LOG=debug cargo run: ShowsDEBUGand higher messages.RUST_LOG=trace cargo run: Shows all messages, includingTRACE.RUST_LOG=my_ssg_crate=debug cargo run: If your crate is namedmy_ssg_crate, this only showsDEBUGmessages for your code.RUST_LOG=my_ssg_crate=debug,tera=info cargo run: Shows debug for your crate, but only info for theteracrate.
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
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.
- 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
Performance & Logging Overhead:
- Log Levels: In production, set
RUST_LOG=infoorRUST_LOG=warnto minimize logging overhead.debugandtracelevels 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 inCargo.tomlcomments) 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.
- Log Levels: In production, set
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.
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!orwarn!messages are detected during a build, indicating potential issues with content or templates.
- Structured Logs:
Code Review Checkpoint
At this stage, we have significantly improved the robustness and diagnosability of our SSG.
Summary of what was built:
- Introduced
thiserrorfor defining custom, strongly-typed error variants for specific failures within our SSG modules. - Integrated
anyhowfor simplified application-level error handling, allowingmainto gracefully catch and report diverse errors. - Set up the
tracingecosystem 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, andsrc/renderer.rs(example) to incorporate the new error handling and logging patterns.
Files created/modified:
Cargo.toml: Addedanyhow,thiserror,tracing,tracing-subscriber.src/error.rs: New file definingAppErrorenum.src/main.rs: Initializedtracing-subscriber, importedAppError, modifiedmainto useanyhow::Resultand call a build function returningAppError.src/parser.rs: Modifiedparse_content_fileto returnResult<ParsedContent, AppError>and usetracingmacros.src/renderer.rs(example): ModifiedSiteRenderermethods to returnResult<T, AppError>and usetracingmacros.
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
Issue: “The
?operator can only be used in a function that returnsResultorOption(or another type that implementsTry)”.- Problem: You’re trying to use
?in a function that doesn’t declare aResultreturn type, or the error type doesn’t match. - Solution: Ensure your function signature is
fn my_func() -> Result<T, AppError>(oranyhow::Result<T>). Also, if you use?on aResult<T, E_other>, ensureE_othercan be convertedintoyour function’s return error type (AppErrorin our case). IfE_otherisstd::io::Error,#[from] std::io::ErrorinAppErrorhandles this automatically. If not, you might needmap_erras shown withserde_yaml::Error.
- Problem: You’re trying to use
Issue: “Logs are not appearing in the console”, or “Only
error!messages are showing”.- Problem: The
tracing-subscribermight not be initialized, or theRUST_LOGenvironment variable is set too restrictively. - Solution:
- Verify
fmt::Subscriber::builder().with_env_filter(...).init();is called exactly once at the very beginning ofmain. - Check your
RUST_LOGenvironment variable. If it’sRUST_LOG=error, only error messages will show. TryRUST_LOG=infoorRUST_LOG=debugto see more. - Ensure you have
tracing-subscriberwithenv-filterandfmtfeatures enabled inCargo.toml.
- Verify
- Problem: The
Issue: “My custom error variant requires a
PathBuf, but I only have a&Path.”- Problem:
Pathis a borrowed type,PathBufis owned. Error types often need to own their data. - Solution: Use
file_path.to_path_buf()to create an ownedPathBuffrom a borrowed&Path.
- Problem:
Testing & Verification
To verify our error handling and logging, we need to intentionally introduce errors and observe the SSG’s behavior and its output.
Trigger
IoError:- Modify
src/main.rsto attempt to load aconfig.tomlfrom 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.
- Modify
Trigger
FrontmatterError:- 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.
- Create a file
Trigger
TemplateError:- 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.
- Modify a content file’s frontmatter to specify a non-existent template, e.g.,
Verify Log Levels:
- Run
cargo run(defaultINFOlevel). You should seeinfo!andwarn!/error!messages. - Run
RUST_LOG=debug cargo run. You should now see alldebug!messages appearing, providing much more detail about file parsing and rendering. - Run
RUST_LOG=error cargo run. Onlyerror!messages should be visible.
- Run
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.