How do you propagate warnings?

I don't mean compilation warnings, but issues at application-level logic at runtime, where something isn't quite right, but it's not as bad to be an error. Like Result, but for warnings, not errors.

Rust programs are doing great with propagation of function-terminating errors, and there are many conventions for signaling and handling of errors.

But when I want to say "the operation succeeded, but I had to recover from some errors" or "here's the result, but some data was missing"?

The simplest approach is to do:

warn!("Improvising!");

but that's suitable only for some CLI applications. In a context of a web server, for example, I may want to print warnings in HTML, or in an HTTP header, or insert in a field of a JSON response.

I've tried returning warnings as part of the result:

fn doit() -> Result<(T, Vec<Warning>), Error> {}

but that was cumbersome to use when I wanted to aggregate warnings from several function calls:

let mut all_warnings = Vec::new();
loop {
    let (res, mut tmp) = doit()?;
    my_warnings.append(&mut tmp);
    …
}
Ok(my_result, all_warnings)

I've also tried:

fn doit(warnings: &mut Vec<Warning>) -> Result<T, Error> {}

which was easier to use, but &mut gets in the way of parallelization. So I ended up with:

fn doit(warnings: Arc<WarningsCollector>) -> Result<T, Error> {}

that does locking internally itself.

Has anyone been looking into this? Are there other, better approaches?

I've approximately gone with your first approach, but reuse the same error type. I'm not completely happy with it, but I don't think the explicit propagation is that bad. I definitely would like to revisit this at some point, and I do kind of like your idea of explicitly separating warnings and errors at the type level. That does make the arrangement a bit more explicit.

I've been going with this approach in rust-content-security-policy. I'm not currently doing anything thread-based in there, but if I was going to add that, what would stop me from using this approach in most of the code while leaving the thread synchronization entirely in some top-level event handler? Like this, basically?

fn doit_parallel(tasks: &[Task], warnings: &mut Vec<Warning>) {
let mut join_handlers = Vec::new();
for chunk in tasks.chunks(32) {
    join_handlers.push(thread::spawn(|| {
        let mut local_warnings = Vec::new();
        doit(chunk, &mut local_warnings);
        local_warnings
    });
}
for local_warnings in join_handlers.into_iter().map(|j| j.join()) {
    if let Ok(local_warnings) = local_warnings {
        warnings.append(local_warnings);
    } else {
        warnings.push(Warning::ThreadPanicked);
    }
}
}

There are three very good reasons to do this:

  • It leaves rust-content-security-policy completely agnostic to the existence of threads.
  • No lock contention, no false sharing, no reference counting.
  • It prevents warnings from becoming interleaved.

In C# I do this by taking an Action<Warning>; in Rust that would be an impl FnMut(Warning).

That way different parts of my code can ignore them, panic on them, put them in a list, log them, or whatever.

2 Likes

You don't. Most warnings are intended for server administrator and should go to logs. Anything intended for request consumer should be part o whatever protocol you are using.

I'm pretty sure I do, since my protocol is HTTP (not every server is RPC over JSON).

1 Like

Interesting about interleaving of the warnings. But still the temporary Vecs look unappealing to me. Maybe something like warnings: impl VecLike<Warning>, so that it'd accept both Vec or something fancier.

I don't think it's that bad to pass |w| warnings.push(w) instead of &mut warnings.

(If only warnings.push worked...)

One thing addressed, but not called out, in your other solutions that this one omits: Warnings could happen on the path to an Err (and might help diagnose it), as well as on the path to an Ok.

My first take would have been more like:

fn doit() -> (Result<T, Error>, Warnings) {}

where warnings was a type that impl Iterator, and so could be easily checked for emptiness (Options behave as iterators too and set some idiomatic precedent that can be borrowed nicely) or chained to. That would have still been a little clunky aggregating in the caller, but perhaps slightly less so. (Edit: of course Vec is such a type, but I was thinking of one with purpose-specific convenience methods)

Musing further: Your other forms allow warnings to be added even when there are errors, but there's still a potential assumption that an Ok means the warnings don't need to be checked. So perhaps something like your first form is actually most correct, when combined with a convention that error returns should collect any warnings that were present at the time, as part of their context? To work with the ? operator nicely, you'd need some trick with the From trait that ? uses and some kind of accessible warning state stashed somewhere.

Stepping back: don't all warnings basically start out as lower-level errors (config file not found) that are then handled by higher-level recovery code (create a template/default config file), but which event the code in question still wants to call attention to (in case this wasn't your first run and your config has gone missing).

So I think this is closer to a logging-framework question, even if the actual problem in your example is how to annotate http responses with suitable logging context. In other words, it's not just warnings, perhaps you want (to be able to enable) info and debug levels as well? If so, structured logging approaches work really well for this.

1 Like

This is (essentially) the way I've dealt with warnings when writing something like a parser/compiler. Most functions will be passed in an &mut Diagnostics (wrapper type around Vec<Diagnostic>) and then after the function returns, or at convenient places in the program, I'll check to see if there were any errors, notes, or warnings, and handle them appropriately.

On another note, I think the stance Go takes towards compiler warnings has some merit:

There are two reasons for having no warnings. First, if it's worth complaining about, it's worth fixing in the code. (And if it's not worth fixing, it's not worth mentioning.) Second, having the compiler generate warnings encourages the implementation to warn about weak cases that can make compilation noisy, masking real errors that should be fixed.

While it may be worth fixing, it might not be worth fixing right now. See, for example,

Basically, rustc is willing to make things warnings if they're unnecessary not perhaps not wrong.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.