-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
Motivation
Not surprisingly, observing real-life projects shows that using error enums with all unit variants is the preferred way to model errors. In terms of code readability, maintenance, etc. it is definitely the right abstraction for error modelling.
A current disadvantage of this approach is that it doesn't play well with the panic expression. A very typical code like:
panic OrderCreationError::InvalidAsset;
will end up logging OrderCreationError::InvalidAsset. This means encoding the variant, and calling log. Even after #7496 we can still have a situation of enclosing enum type not being trivially encodeable (some variants might be non-unit). And even for trivially encodeable types, we will still call a logd instruction and create a receipt for a case where having only the corresponding error message in case of panicking is sufficient and very likely expected behavior.
Currently, the only way to avoid the overhead of logging is to use hardcoded strs:
panic "The provided asset was invalid.";
This breaks the error enums pattern and can lead to unwanted code duplications (currently strs cannot be reusable constants). E.g., the above OrderCreationError::InvalidAsset appears twice in the inspected code, and some OrderCreationError variants more than twice.
Proposed solution
log_panic_values build option
To support the enum error pattern but with zero cost by default or opted-in cost if desirable, the proposal is to implement the log_panic_values build option and the #[log_panic_values] attribute. Those would serve the similar purpose like the backtrace build option and the #[trace] attribute for the ABI backtracing - empowering developers to opt-in or out of a cost.
log_panic_values build option would have the below values that govern the compiler's behavior when it encounter the code like this:
panic SomeError::SomeUnitVariant;
panic SomeError::SomeNonUnitVariant(variable);
| Value | Meaning |
|---|---|
| only_non_unit | Log enum variant only if it is a non-unit one. This is the default option for the release build. It ensures that a potentially relevant information is logged. For unit values, we assume that the error message, location, and the backtrace is what the end dev wants to see, and the unit enum variant is superfluous. |
| always | Always log enum variant. This is the default option for the debug build. |
| never | Never log enum variant. This option strips off any logging overhead, but stills keeps the zero-cost error message and panic location as well as negligible-cost backtrace if wanted. |
[log_panic_values(<option>)] attribute
The #[log_panic_values(<option>)] attributes could be applied on functions to locally override the build option. The override applies only on panic expressions in that function, not in the functions it calls. We emit a warning if the attribute is applied on a function that does not have any panic expression.
The <option>s for attribute are the same as for the build option, with addition of one option exclusive to the attribute: never_on_only_non_unit.
never_if_only_non_unit attribute argument
never_if_only_non_unit would locally override the only_non_unit option and turn it into never instead. This is useful for library development where we want to provide rich errors with arguments in non-release builds, but go to zero-cost message in release build.
E.g., in the case of overflow, the <u8 as Add>::add could emit the actual numbers being added together with the message in the debug build:
panic U8OpsError::Overflow((a, b));
In the release build the above line will always compile to just a single rvrt.
Note that in the case of panicking an a variant unknown at compile-time, we by default log, unless the applicable log_panic_values is never (or never_if_only_non_unit when the build option is only_non_unit). E.g.:
panic some_error_var;
Drawbacks
The only drawback I see is the increased overall complexity of the panic language feature.
However, the meaningful defaults provide expected behavior for the majority of usages. Similar to the backtrace build option and the #[trace] attribute, most of the developers will never need to use the feature. Zero-cost will just be there.
Still, in advanced scenarios and library development, they can benefit from having the full control over the logging cost of panic expression.