Async I/O
v0.4.0 introduces optional Tokio-backed async counterparts for DotCfg and ConfigStack loading, saving, and key access.
Start with async loading and saving, async key access, or async ConfigStack. Each guide links back to the corresponding configuration rules.
Enable the feature
[dependencies]
dotcfg = { version = "0.4.0", features = ["async"] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }The async feature enables Tokio file I/O inside dotcfg. Your application also needs a Tokio runtime to call the methods with .await. TOML remains the default format; JSON and YAML can be enabled alongside async.
One file with DotCfg
Call load_async() when a missing file is acceptable, or load_or_error_async() when it is required. load_or_default_async() writes a default value when the file is missing.
use dotcfg::DotCfg;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct AppConfig {
port: u16,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cfg = DotCfg::new("myapp");
let previous: Option<AppConfig> = cfg.load_async().await?;
cfg.save_async(&AppConfig { port: 8080 }).await?;
let port: u16 = cfg.get_as_async("port").await?;
println!("Previous config: {}, current port: {port}", previous.is_some());
Ok(())
}For individual keys, use get_async() and set_async() for strings, or get_as_async() and set_val_async() for typed values. save_async() writes a complete config. Async file discovery, existence checks, and deletion are also available on DotCfg.
Read async loading and saving for missing files and complete structs, then async key access for strings, typed values, and nested keys.
Layered files with ConfigStack
Build the stack just as you would for synchronous use. Async reads still merge earlier and later layers, with environment overrides on top. Async writes still go only to the source selected by write_to().
use dotcfg::{ConfigStack, DotCfg};
let defaults = DotCfg::new("myapp").at_dir("./config/defaults");
let project = DotCfg::new("myapp").at_dir("./config/project");
let cfg = ConfigStack::new()
.layer("defaults", defaults)
.layer("project", project)
.write_to("project")
.with_env_prefix("APP")
.build()?;
let port: u16 = cfg.get_as_async("server.port").await?;
println!("Current port: {port}");
cfg.set_val_async("server.port", 8080u16).await?;ConfigStack also has get_async(), get_as_async(), load_async(), load_or_error_async(), set_async(), set_val_async(), and save_async(). It has no load_or_default_async(): the stack does not choose a file to create when every source is missing. To work with one named source directly, call cfg.source("project")? and then a DotCfg async method.
The async ConfigStack guide covers resolved reads, write targets, and direct source access.