source traversal != backtrace
Ah yeah, I was thinking of the "Caused by" shenanigans, which it prints in addition to the stacktrace.
Since context is kind of on topic, what should one write there? Are there any principles for writing good context messages?
fn beebboop() {
foo();
bar().context("frobnicating");
baz();
}
Instead of "frobnicating" in this rough example, I could also write that we intend to "baz" afterwards or that we are "currently beebbooping" or "bar failed" or I could even mention "foo" or ...
From my (rather limited) experience, it seems most useful to describe what beebboop is about to do. Sometimes that is weird though, because then the context for foo, bar, and baz calls would be essentially the same.
I've kind of standardized on writing it as "Failed to bar when beebbooping, while fadoodling."
Maybe a more concrete example:
Failed to write file when persisting audio, while recording. Caused by: Permission denied.
The while-part is optional, in case you've got a larger context.
Well, so is the rest of it, really. I may just write "Error when beebbooping", if I believe the error from .bar() to describe itself rather well. Error handling is hard. 🫠
But yeah, you kind of have to describe that an error occurred, rather than purely describing what you want to do, because anyhow doesn't prefix it or such.
I just optimized the context messages and I'm now happy with:
Error: Execute command: redo workspace/motd.json
Caused by:
0: using sqlite db 'workspace/.redo3.db'
1: clear dependencies for motd.json
2: attempt to write a readonly database
3: Error code 8: attempt to write a readonly database
The "0" context is especially important because it tells you which file is read-only. Here is the code for that:
pub(crate) fn start(mut self) -> Result<DoFileExecution> {
let rc_folder = self.target.path.parent().unwrap_or(Utf8Path::new("."));
let mut db = Db::try_open(rc_folder);
db.transaction(|tx| {
tx.clear_dependencies(self.target.basename())
.with_context(|| format!("using sqlite db '{}'", rc_folder.join(DB_FILENAME)))
})?;
let now = chrono::Utc::now();
...
I don't think something like "failed to clear dependencies using sqlite db '{}'" would be helpful. Something like "failed to" or "error when" does not really add any information. Just describe what is happening. Also, the "clear dependencies" would be redundant because that function can handle that part itself (see msg 1).
Adding context does two things:
- Add additional messages to the error for debugging purposes
- Attach data to the error for the callers
For the former, I prefer all lowercase, very concise, and with the intent to describe what beebboop was doing when it failed (in your example). More practically, what produced the error could be writing to the filesystem, and the additional context could be that the filesystem was being written to because it was saving a config file.
For the latter, this is actually a lot different. anyhow lets you downcast its errors into any of its contexts. You can use this to create aggregated errors, test for specific contexts, or attach useful details to the error which might help with recovering from it.
Other useful Debug implementations:
PathBufandPathwill give you the path with quotes and I believe, it escapes any non-UTF8 characters.std::time::Durationwill give you reasonably formatted output, likeDuration::from_millis(54321)will format as54.321s, whereasDuration::from_millis(321)will format as321ms. Not appropriate for every situation, but works pretty well for logging.
The error returns a stack trace like this because if you return Err(anyhow::Error) from your main function, it debug prints the error when it exits.
As you mentioned, it's very convenient during logging! The Display impl shows much less information, so usually you want the Debug info of anyhow::Error during logging.
Rust Programming