Crate tighterror
source ·Expand description
tighterror is a minimalistic error representation framework with compact error types and a code generator.
It is heavily inspired by the standard std::io::Error
which
represents a group of errors in a single Rust type.
This concept is taken one step further so that a tighterror can represent
multiple groups of errors in a single Rust type.
tighterror is designed to achieve the following goals:
- Minimal error type size, e.g.,
u8
or ZST, while still supporting idiomatic error reporting. - Minimal runtime overhead on error path. No dynamic memory allocation.
- Minimal runtime overhead in matching a whole logical group of errors.
- Minimal coding overhead:
- specify errors in markup language and generate all the boilerplate Rust code automatically
- ergonomic error reporting
§Table Of Contents
- High-Level Overview
- Implementation Details
- Specification File Reference
- tighterror-build
- cargo-tighterror
- Motivation
§High-Level Overview
In its basic form a tighterror is a newtype around a small unsigned
integer, e.g., u8
.
Error conditions are identified by an error kind
.
Every error kind has a category
that
identifies the logical group the error belongs to.
#[repr(transparent)]
pub struct ErrorKind(u8);
#[repr(transparent)]
pub struct ErrorCategory(u8);
#[repr(transparent)]
pub struct Error(ErrorKind);
The tighterror crate defines only the “public interface” of these structs, and associated types:
TightErrorCategory
forErrorCategory
TightErrorKind
forErrorKind
TightError
forError
The concrete types are auto-generated from a specification file defined in
the user project. The specification file is written in one of the supported
markup languages. For example, tighterror.yaml
:
---
errors:
- BadFile
- BadArg
The specification file is translated to Rust using a dedicated command
line tool cargo-tighterror
or
using the tighterror-build
library:
cargo tighterror -o src/errors.rs
This creates a module with all the concrete Rust types and
corresponding trait implementations. The error categories and kinds are
placed in the categories
and kinds
sub-modules respectively:
pub mod categories {
pub const GENERAL: ErrorCategory = ErrorCategory::new(0);
}
pub mod kinds {
pub const BAD_FILE: ErrorKind = ErrorKind::new(categories::GENERAL, 0);
pub const BAD_ARG: ErrorKind = ErrorKind::new(categories::GENERAL, 1);
}
Error
is the main type to be used in Result<T, E>
.
It can be created from an error kind constant like so:
let e: Error = BAD_FILE.into();
assert!(matches!(e.kind(), BAD_FILE));
assert!(matches!(e.category(), GENERAL));
§Implementation Details
Error kind is implemented with an unsigned integer (rather than an enum). This allows us to embed some additional information in its bits.
It is composed of two main parts - error category and error variant. Error category denotes the group of errors a specific error kind belongs to. Error variant is a unique identifier of the error within its category.
The following is an example layout of an error kind with allocation of bits suitable for 4 categories with a maximum of 32 errors within each category:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │ 0-1 2 category bits
u8│ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 │ 2-6 5 variant bits
│ │ │ │ │ │ │ │ │ 7-7 1 reserved bit
└───┼───┴───┴───┴───┴───┼───┴───┤
│ variant │ cat │
The number of variant bits is large enough to accommodate the largest category. Therefore some categories may have more reserved bits than others, assuming not all categories are of the same length.
When there is only a single error category the number of category bits is 0.
By default tighterror chooses the smallest unsigned integer that is
large enough to accommodate a required layout. Supported underlying
types are u8
, u16
, u32
and u64
.
Note that the layout of error kind is packed and may change during project’s lifetime. For example, addition of a new error category may lead to a left-shift of variant bits, thus changing the numeric value of all error kinds of the project, and possibly changing the underlying Rust type to a wider unsigned integer. Consequently, error kinds should be matched only using named constants defined by their origin crate. No assumption should be made about a named constant having any specific numeric value.
§Specification File Reference
tighterror uses markup languages for its specification file. Currently supported languages are YAML and TOML.
§Filename
When specification filename isn’t explicitly provided tighterror-build
searches the current working directory for a file with one of the default
names tighterror.yaml
or tighterror.toml
.
Custom filenames, or files outside the current working directory,
must be explicitly specified. A custom filename must have one of the
supported markup language extensions .yaml
or .toml
.
§Error Object
An error object is comprised from the following attributes:
name: <string> (required)
display: <string>
doc: <string>
doc_from_display: <bool>
-
name
- string (required)Defines the error kind name. Must be a string in UpperCamelCase. This string becomes an error kind constant after conversion to UPPER_SNAKE_CASE.
-
display
- string (optional)Defines the error kind’s display string. This string is used in
std::fmt::Display
implementation to display the error kind.
When undefined the error kind’s UPPER_SNAKE_CASEname
is used as display string. -
doc
- string (optional)Defines the error’s documentation comment. This becomes the doc comment of the error kind’s constant. When undefined the constant doesn’t receive a doc comment.
-
doc_from_display
- bool (optional)When enabled the
display
string is also used as the doc comment, unlessdoc
is defined. Thedisplay
attribute must be explicitly set for this to take effect.This attribute can be set on a higher level in category specification to affect all errors in the category, or in module specification to affect all errors in the module. Values defined on lower levels win.
Default:false
An error object never appears standalone. It must be defined as an item in an error list.
§Error Object Examples
YAML
errors:
- name: BadPath
display: Path is malformed.
doc: Returned when a path value has an invalid structure.
doc_from_display: false
TOML
[[errors]]
name = "BadPath"
display = "Path is malformed."
doc = "Returned when a path value has an invalid structure."
doc_from_display = false
§Error List
Error list is an ordered list of error objects
with unique names. It is identified by the errors
keyword.
The errors
keyword may appear as a standalone top-level attribute or as
an attribute of a category object.
When specified at the top-level the specification file defines only a single
category and the list defines all the errors of the specification.
When specified at category object level the specification file can define
more than one category, and the list defines errors belonging to the
category in which it is specified. The top-level and per-category notations
are mutually exclusive.
To allow more compact specification of errors tighterror supports shorthand notations where part of error object attributes are specified without corresponding keywords (see the examples below).
The order of elements in a list matters because error variant is assigned to errors in the order of their appearance in the list. Therefore, insertion or deletion of a not-last object causes reassignment of error variant of all subsequent objects in the list. This may create a larger than expected diff in the generated code.
§YAML Error List
In general an error list is an array of error objects where every
item can have any of the error object’s attributes.
The name
attribute is mandatory.
---
errors:
- name: MissingAttribute
display: An object attribute is missing.
doc_from_display: true
- name: Timeout
display: Operation timed out.
doc_from_display: true
Name-Only Notation
Error object has only a single mandatory attribute, the name
.
This allows a short name-only notation, where every element
of the list is an error name specified without the name
keyword.
This notation doesn’t allow specification of attributes other than name
.
Therefore they receive default values. In particular, an error kind
constant has no doc comment and is displayed as UPPER_SNAKE_CASE of the
name.
---
errors:
- MissingAttribute
- Timeout
Name-Display Notation
Name-display notation slightly enhances the name-only notation
with an addition of a display string, without using the display
keyword. Every element in the list is a name: display
single-item mapping.
This notation is available in YAML only.
---
errors:
- MissingAttribute: An object attribute is missing.
- Timeout: Operation timed out.
§TOML Error List
In TOML an error list is an array of tables:
[[errors]]
name = "MissingAttribute"
display = "An object attribute is missing."
doc_from_display = true
[[errors]]
name = "Timeout"
display = "Operation timed out."
doc_from_display = true
or array of inline tables:
errors = [
{ name = "MissingAttribute", display = "An object attribute is missing.", doc_from_display = true },
{ name = "Timeout", display = "Operation timed out.", doc_from_display = true }
]
Name-Only Notation
Name-only shorthand notation is available in TOML as follows:
errors = ["MissingAttribute", "Timeout"]
§Category Object
A category object is comprised from the following attributes:
name: <string> (required*)
doc: <string>
doc_from_display: <bool>
errors: <ErrorList> (required*)
-
name
- stringDefines the name of the category.
The value must be a valid Rust struct name specified in UpperCamelCase. This string becomes a category constant after transition to UPPER_SNAKE_CASE.
The name is a mandatory attribute when category object is defined as an item in a category list. Otherwise it is optional with the default value
General
. -
doc
- string (optional)Defines the category’s document string.
This becomes the doc comment of the category constant.
-
doc_from_display
- bool (optional)Sets a default value for the
doc_from_display
error object attribute. This affects errors belonging to this category only. The value is used for all errors that do not define it explicitly as part of error object specification.Usage of this attribute allows addition of a doc comment in name-display error list notation, which otherwise isn’t possible.
-
errors
- ErrorListDefines the list of errors belonging to this category.
This is a mandatory attribute when category object is defined as an item in a category list. Conversely, when a category object is defined as a standalone top-level attribute (see below) this attribute is forbidden, and the error list must be defined as a top-level attribute.
See the error list section for more information.
A category object can appear as a standalone top-level attribute
category
or as an item in a category list.
Definition at the top-level allows specification of a single category.
Definition as an item in a category list allows specification
of multiple categories.
When a category object is specified at the top-level its errors
attribute is forbidden, it must be defined at the top-level instead.
Therefore, the whole category object at the top-level is
optional because all other attributes have default values.
When no category object is defined tighterror creates an implicit
General
category.
When there is only a single category the number of category bits is zero.
See category list documentation for more information.
§Category Object Examples
A specification with a single implicit category General
. Note, the error
constants have no documentation in this scenario.
---
errors:
- MissingAttribute: An object attribute is missing.
- Timeout: Operation timed out.
A specification with a single category with a custom name. Note, the error constants receive a doc string that is equal to the display string.
---
category:
name: MyErrorCategory
doc_from_display: true
errors:
- MissingAttribute: An object attribute is missing.
- Timeout: Operation timed out.
A TOML specification with a single category with a custom name and documentation equal to display string.
[category]
name = "MyErrorCategory"
doc_from_display = true
[[errors]]
name = "MissingAttribute"
display = "An object attribute is missing."
[[errors]]
name = "Timeout"
display = "Operation timed out"
§Category List
Category list is an ordered list of category objects
with unique names. It is identified by the categories
keyword.
The categories
keyword may appear as a standalone top-level attribute of a
specification file. It allows definition of one or more categories, each
with its own set of errors.
Note that when a category object is defined as an item in a
category list the name
and errors
attributes are mandatory.
Also note that category
and categories
keywords are mutually exclusive,
as well as top-level errors
and categories
keywords. category
and
top-level errors
keywords are used only for a single-category
specification.
The order of elements in a list matters because category constant identifier is assigned in the order of their appearance in the list. Therefore, insertion or deletion of a non-last category causes reassignment of category identifiers of all subsequent objects in the list. This may create a larger than expected diff in the generated code.
§Category List Examples
A two-category YAML example.
---
categories:
- name: Parsing
doc_from_display: true
errors:
- MissingAttribute: An object attribute is missing.
- InvalidAttribute: An object contains an invalid attribute.
- name: CodeGeneration
doc_from_display: true
errors:
- InvalidName: An object has an invalid name.
- InvalidPath: A file path is invalid.
An equivalent two-category TOML example.
[[categories]]
name = "Parsing"
doc_from_display = true
[[categories.errors]]
name = "MissingAttribute"
display = "An object attribute is missing."
[[categories.errors]]
name = "InvalidAttribute"
display = "An object contains an invalid attribute."
[[categories]]
name = "CodeGeneration"
doc_from_display = true
[[categories.errors]]
name = "InvalidName"
display = "An object has an invalid name."
[[categories.errors]]
name = "InvalidPath"
display = "A file path is invalid."
§Module Object
The module object is identified by the module
keyword
and is found at the top-level of a specification file.
It defines attributes of objects that are present at the Rust module level
and below.
All attributes of the module object have default values. Therefore,
the whole module
section is optional.
---
module:
doc: <string>
doc_from_display: <bool>
err_cat_doc: <string>
err_cat_name: <string>
err_doc: <string>
err_kind_doc: <string>
err_kind_name: <string>
err_name: <string>
error_trait: <bool>
flat_kinds: <bool>
result_from_err: <bool>
result_from_err_kind: <bool>
-
doc
- string (optional)Defines the doc comment of the generated module.
By default module doc comment is not defined. -
doc_from_display
- bool (optional)Sets a default value for the
doc_from_display
error object attribute. This value is used for all errors that do not define it explicitly as part of error object specification.Usage of this attribute allows addition of a doc comment in name-display error list notation, which otherwise isn’t possible.
--- module: doc_from_display: true errors: - MissingAttribute: An object attribute is missing. - Timeout: Operation timed out.
-
err_cat_doc
- string (optional)Defines the doc comment of the error category struct.
This becomes the doc comment of the struct that implements the TightErrorCategory trait.
The default value is equivalent to the following YAML specification:--- module: err_cat_doc: | Error category type. See the [categories] module for category constants.
-
err_cat_name
- string (optional)Defines the name of the error category struct.
This becomes the name of the struct that implements the TightErrorCategory trait.
The value must be a valid Rust struct name.
Default:ErrorCategory
-
err_doc
- string (optional)Defines the doc comment of the error struct.
This becomes the doc comment of the struct that implements the TightError trait.
The default value is equivalent to the following YAML specification:--- module: err_doc: | Error type. See the [kinds] module for error kind constants.
-
err_kind_doc
- string (optional)Defines the doc comment of the error kind struct.
This becomes the doc comment of the struct that implements the TightErrorKind trait.
The default value is equivalent to the following YAML specification:--- module: err_kind_doc: | Error kind type. See the [kinds] module for error kind constants.
-
err_kind_name
- string (optional)Defines the name of the error kind struct.
This becomes the name of the struct that implements the TightErrorKind trait.
The value must be a valid Rust struct name.
Default:ErrorKind
-
err_name
- string (optional)Defines the name of the error struct.
This becomes the name of the struct that implements the TightError trait.
The value must be a value Rust struct name.
Default:Error
-
error_trait
- bool (optional)When enabled implements
std::error::Error
trait on the error struct.
This attribute is ignored whenno_std
is enabled.
Default:true
-
flat_kinds
- bool (optional)Puts the error kind constants directly under the
kinds
sub-module instead of underkinds::<category-module-name>
sub-sub-module.By default error kind constants are placed in a category-specific sub-module. For example:
categories: - name: Foo errors: - Err - name: Baz errors: - Err
The error kind constants’ paths are
kinds::foo::ERR
andkinds::baz::ERR
. The category-specific sub-modulesfoo
andbaz
are required because by default error names are required to be unique on category level only.There are cases when error names are unique per module. In these cases having the category-specific module in the constants’ paths has no practical function and just makes the paths longer.
The
flat_kinds
attribute forces tighterror to put the constants directly under thekinds
sub-module, e.g.,kinds::ERR
. Enabling this option requires the error names to be unique on module level (and not only on category level).module: flat_kinds: true categories: - name: Foo errors: - Err - name: Baz errors: - AnotherErr
Default:
false
-
result_from_err
- bool (optional)When enabled an implementation of From trait is added to create a
Result<T, Error>
fromError
.
Default:true
-
result_from_err_kind
- bool (optional)When enabled an implementation of From trait is added to create a
Result<T, Error>
fromErrorKind
.
Default:true
§Main Object
The main configuration object is identified by the main
keyword and is found at the top-level of a specification file. It defines
attributes that affect the code generation globally, applicable to all
other specification objects in the file.
All attributes of the main object have default values. Therefore, the
whole main
section is optional.
The main object is comprised of the following attributes:
main:
no_std: <bool>
output: <string>
-
no_std
- bool (optional)Generates code suitable for Rust
no_std
environment.When enabled this attribute implicitly disables
error_trait
and skips unit tests that requirestd
.
Default:false
-
output
- string (optional)Defines the output file path. When undefined the output is written to
stdout
.The value
-
forces the output to be written tostdout
.This attribute is overridden by the
-o, --output
command-line argument in cargo-tighterror.
§tighterror-build
tighterror-build is a library that implements tighterror
specification file parsing, processing, and code generation.
The library is usually not a direct dependency of a project that uses the
tighterror framework, i.e., it is needed at project’s build time,
rather than at project’s runtime.
The recommended way to use it is through
cargo-tighterror
command-line utility.
In special cases, when the cargo plugin cannot be used, the library can be
used as a build-dependency of the project, i.e., in build.rs
.
See the library documentation for more details.
§cargo-tighterror
cargo-tighterror
is the cargo plugin of the framework.
It is a thin wrapper around the tighterror-build
library.
Install the plugin, or upgrade to the latest version, using the following command:
cargo install cargo-tighterror
Note that the plugin needs to be periodically upgraded to follow
releases of tighterror-build
.
The plugin is the recommended way to process tighterror specification files and generate code. It allows a project to generate code in pre-build stage so that both the specification file and the generated code are under source control. Additionally, the plugin allows to override part of the settings defined in the specification file.
$> cargo help tighterror
The cargo plugin of the tighterror framework.
Usage: cargo tighterror [OPTIONS]
Options:
-s, --spec <PATH> The specification file path
-o, --output <PATH> The output file path
-t, --test Include a unit-test in the generated code
-u, --update Do not overwrite the output file if data is unchanged
-h, --help Print help
-V, --version Print version
-
-s, --spec <PATH>
(optional)Defines the specification file path.
It is required if a specification file has a custom name. It can be omitted if the specification file uses the default name
tighterror.yaml
ortighterror.toml
and is present in the current working directory.Note that the file extension
.yaml
or.toml
is mandatory in custom filenames. -
-o, --output <PATH>
(optional)Defines the output file path.
This argument overrides the
output
attribute defined in the specification file. If this argument is omitted and output file path isn’t defined in the specification file the output is written tostdout
.Use hyphen
-
to force the output to be written tostdout
. -
-t, --test
(optional)Includes a unit-test in the generated Rust code.
In
no_std
environments test cases that requirestd
are excluded. -
-u, --update
(optional)Enables the update mode.
By default the output file is overwritten unconditionally, even when the new data equals the already existing one. This may cause an unnecessary crate rebuild, because file’s write timestamp is updated. In update mode the new data is compared to the existing one, and the output file is overwritten only if the new data is different. Therefore, the file’s write timestamp is updated only if the file content has changed, i.e., a rebuild is indeed required.
§Motivation
Error handling in general and error representation/reporting in particular are non-trivial topics in many programming languages.
Rust simplifies error reporting by introduction of Result
which is
must_use
. This is great, as forgetting to check an error condition
becomes a compilation error. However, a programmer still has a lot of
decisions to make.
This rather large section describes the reasons for tighterror creation.
§Error as Trait Object
Lets take a look at a function with multiple error conditions:
use std::{error::Error, fmt::Write, fs::File, io::Read};
fn foo() -> Result<String, std::io::Error> {
let mut f = File::options().read(true).open("/etc/hosts")?;
let mut hosts = String::new();
f.read_to_string(&mut hosts)?;
let mut v = String::new();
write!(v, "hosts = {:?}", hosts)?;
Ok(v)
}
This function has three error cases: File::open
on line 4,
Read::read_to_string
on line 6, and write!
on line 8.
The first two have io::Error
as the error type, while the third has
fmt::Error
.
As a consequence this function doesn’t compile because the error type
fmt::Error
is not implicitly convertible to io::Error
by the ?
operator:
---- src/lib.rs - (line 37) stdout ----
error[E0277]: `?` couldn't convert the error to `std::io::Error`
--> src/lib.rs:45:37
|
5 | fn foo() -> Result<String, std::io::Error> {
| ------------------------------ expected `std::io::Error` because of this
...
10 | write!(v, "hosts = {:?}", hosts)?;
| ^ the trait `From<std::fmt::Error>` is not implemented for `std::io::Error`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
...
error: aborting due to previous error
So, what should be the error type of foo
?
One solution is to return a type that can “wrap” any type
that implements the std::error::Error
trait.
For example, Box<dyn Error>
:
use std::{boxed::Box, error::Error, fmt::Write, fs::File, io::Read};
fn foo() -> Result<String, Box<dyn Error>> {
let mut f = File::options().read(true).open("/etc/hosts")?;
let mut hosts = String::new();
f.read_to_string(&mut hosts)?;
let mut v = String::new();
write!(v, "hosts = {:?}", hosts)?;
Ok(v)
}
This example compiles successfully because any type that implements
std::error::Error
is convertible to Box<dyn Error>
via the
implementations of From
here
and
here.
A more feature-rich solution is provided by the very popular anyhow
crate:
use std::{fmt::Write, fs::File, io::Read};
fn foo() -> Result<String, anyhow::Error> {
let mut f = File::options().read(true).open("/etc/hosts")?;
let mut hosts = String::new();
f.read_to_string(&mut hosts)?;
let mut v = String::new();
write!(v, "hosts = {:?}", hosts)?;
Ok(v)
}
This works because anyhow::Error
can be created from any type that
implements std::error::Error
as implemented
here.
The common technique to both of these solutions is to “hide” a real error instance behind a trait object allocated on the heap. This works great in the common case, and also preserves the information contained in the original error instance.
However, this convenience has a cost:
- Dynamic memory allocation is made to construct the target error instance.
- The size of the target instance (even without the heap-allocated memory) may be different from that of the original one, and in some cases may be significantly larger, e.g., when the original instance is a ZST.
- Calling methods of the “hidden” instance involves dynamic dispatch.
These costs may be negligible in many projects, but not in all of them.
In tighterror an error condition is identified by a small unique unsigned integer, i.e., an error kind. Conversion into tighterror is very cheap as it entails only a function call that returns a constant value (which in many cases may be inlined).
§Error as Enum
Another solution to support multiple error conditions in a given function
is to enumerate them, i.e., the error type of the function is an enum
which has a distinct variant for every error case of the function.
For example:
use std::{fmt::Write, fs::File, io::Read};
#[derive(Debug)]
enum Error {
IoError(std::io::Error),
FmtError(std::fmt::Error),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{:?}", self)
}
}
impl std::convert::From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Error::IoError(value)
}
}
impl std::convert::From<std::fmt::Error> for Error {
fn from(value: std::fmt::Error) -> Self {
Error::FmtError(value)
}
}
impl std::error::Error for Error {}
fn foo() -> Result<String, Error> {
let mut f = File::options().read(true).open("/etc/hosts")?;
let mut hosts = String::new();
f.read_to_string(&mut hosts)?;
let mut v = String::new();
write!(v, "hosts = {:?}", hosts)?;
Ok(v)
}
This is a lot of coding to support a single function with two error
conditions. Fortunately, we have thiserror
crate which
makes all of this much shorter:
use std::{fmt::Write, fs::File, io::Read};
#[derive(thiserror::Error, Debug)]
enum Error {
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
FmtError(#[from] std::fmt::Error),
}
fn foo() -> Result<String, Error> {
let mut f = File::options().read(true).open("/etc/hosts")?;
let mut hosts = String::new();
f.read_to_string(&mut hosts)?;
let mut v = String::new();
write!(v, "hosts = {:?}", hosts)?;
Ok(v)
}
This code is pretty short and clean. It preserves the information contained in the inner errors, and there is no dynamic memory allocation in translation of inner errors into the outer one.
However, in our opinion, this solution works well only in case of small units with low amount of error conditions (up to three). For a large unit, e.g., a library entrypoint, which can fail on numerous error conditions spread across several layers of abstraction this solution doesn’t scale well for the following reasons:
-
Different layers of abstraction suggest a different error type per layer, each with its own list of error variants. This implies that the top-level error type returned from the library entrypoint has an enum variant per inner abstraction layer.
-
Embedding inner errors as fields of outer errors creates a nested structure which already at the depth of 2 layers becomes hard to deal with, especially on the caller side.
-
The size of inner error types affects the size of outer types in the upper layers. This can easily get out of control when data fields are added to enum variants in the inner layers.
-
Adding meta-data, e.g.,
Location
orBacktrace
, to all errors means adding a corresponding field to every enum variant individually.
tighterror takes a minimalistic approach where there is a single
error type that is capable to represent every error condition of a unit
via a unique error kind, even when errors are spread across different
layers of abstraction. The error type doesn’t convey any additional
error-specific information, except for the optional meta-data,
e.g., Location
or Backtrace
. Representation of 3’rd party tighterrors
is done by dedicating an error category to the 3’rd party type and
embedding its bits as error variant in the outer type.
§Source Code Readability
To mitigate issues 1-3 in error as enum one can use
thiserror
to create error kinds from a simple enum without data fields,
like so:
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("io error")]
IoError,
#[error("formatting error")]
FmtError,
}
This is simple and clean, and can work well even with a high number of error variants in the same enum.
Nevertheless, in our opinion, using constants for error kinds is much more
readable than enum variants. Because constants by Rust coding
convention are written in capital-snake case, e.g., CONNECTION_REFUSED
,
while enum variants are written in upper-camel case, e.g.,
ConnectionRefused
, just like structs, traits, unions and enums themselves.
Thus, using constants on error path is more intuitive and easier to spot
during code review because (a) an error kind is expected to be a constant
value, and (b) it stands out and makes the code less cluttered,
i.e., it doesn’t add more items of the same
case as structs/traits/unions/enums that themselves appear in source code
pretty frequently.
Structs§
- A location in source code.
Traits§
- The trait of error types.
- The trait of error categories.
- The trait of error kinds.