Error handling can be tedious and extermely annoying… On most programming langages, but using Rust it’s actually very simple !

Of course coding methods are a matter of preference and I am nobody to say this is better than something else, but here’s my 2 cents on this subject, and how I prefer to do it.

How I organise error handling in my projects

What I usually do with my projects is that I setup a simple error handling type pub enum Errcode in a file src/, and I define every possible kind of errors in it.

In the whole project, I write any function that can result in an error with a return type of Result<_, Errcode>, allowing me to use the ? operator everywhere, returning the error till it reaches the very main function of my binary.

Error conversion

When I perform a serialization using serde, or some function returning a std::io::Error, How can I transform the error to my “standardized” one ?

Here comes the From trait:

pub enum Errcode {



impl From<serde::ser::Error> for Errcode {
    fn from(e: serde::ser::Error) -> Self {

impl From<serde::de::Error> for Errcode {
    fn from(e: serde::de::Error) -> Self {

impl From<std::io::Error> for Errcode {
    fn from(e: std::io::Error) -> Self {

With this setup, I can use the code abusing the ? operator:

use crate::errors::Errcode;

fn some_serialization(serializable: dyn Serialize) -> Result<(), Errcode> {
    let json_serialized = serializable.to_string()?;
    let deserialized = serde_json::from_str(json_serialized)?;

Avoiding code repetition

If you look at the code block defining all the impl From<_> for Errcode below, you can see how this can be annoying after a while.

I repeated my code more than 2 times, it’s worth spending an hour writing an automation macro ! Luckily for you, I did it for you ;-)

There are 3 parts in the process we want to automate:

  • Create an enum variant with a given name
  • Include a type inside this enum variant
  • Implement the From<_> for Errcode trait on the enum

Getting the arguments of the macro

We will use the pattern [ $( $name:ident : $class:ty ),+ ], meaning that the macro define_errcodes will have the form define_errcodes![ NAME_A : CLASS_A, NAME_B : CLASS_B, ... ]

  • [ .. ] defines the characters before and after the arguments list
  • $( ... ),+ defines the repetition, saying that args are delimited by , and that there is at least one argument.
  • ( A : B ) means that the two arguments in one repetition are separated by a : (it cannot be a , as it would be confusing)
  • $name:ident means $name is an Identifier, same as a variable / function name
  • $class:ty means $class is a Type, it will be checked during the compilation

Generating code for each argument

Inside our macro “code”, we will use some $( <code> )+ blocks, which will loop through all our arguments and generate the <code> for each of them.

So something like:

println!("Code before the loop");
    println!("Name: $name, class: {:?}", $class");

Would generate something like:

println!("Code before the loop");
println!("Name: NAME_A, class: {:?}", CLASS_A);
println!("Name: NAME_B, class: {:?}", CLASS_B);
println!("Name: NAME_C, class: {:?}", CLASS_C);
// ...

Our final macro code

This is now what our src/ file look:

macro_rules! define_errcodes {
    [ $( $name:ident : $class:ty ),+ ] => {
        pub enum Errcode {

            impl From<$class> for Errcode {
                fn from(e: $class) -> Self {

    SerdeSerialization : serde_json::ser::Error,
    SerdeDeserialization : serde_json::de::Error,
    IoError : std::io::Error,

Scoped yet global error handling

The good thing with this macro is that at any particular part of your project you can create a new error type, and will just have to add it to the list !

Then you can have things like:

// src/
    Server : crate::server::ServerError,
    Config : crate::config::ConfigError,
    Cli : crate::cli::CliParseError,
    Network: crate::network::NetworkError,
// src/
pub enum ServerError {

pub struct Server {
    pub fn try_bind_port(&self, port: u16) -> Result<(), ServerError> {
// src/
pub fn init_server() -> Result<(), Errcode> {
    let args = cli.parse_args()?;
    let config = Config::from_args(args)?;
    let server = Server::init(config)?;

See how in the init_server everything is supposed to return a Errcode error case but in our try_bind_port we return a ServerError ? The ? operator will perform the conversion itself, and that allows to write clean and readable code, without all the error-handling-related code.