How do people handle errors in the first draft of a program?

When I'm writing a new program, I typically start by putting together the very basic user interface (command line handling, etc) and a very simplified version of the logic. I then progressively refine the code, adding functionality and handling edge cases as I improve things.

I've used this approach a lot in languages like Python, and it generally works well for me - I can ignore the details until I have the basic structure in place.

To do this in rust, though, I feel like I'd be doing an awful lot of unwrapping of results, letting the code panic if there are problems. Which is fine (it's the equivalent of having an unhandled exception in Python) and I'd just go back later and add error handling. Having explicit error types makes this much nicer, because it's easy to see where I've skipped over error handling.

But when I do come to add error handling later, the need to change return types feels like it makes the job a lot harder - the ripple effects of deciding I want to log a custom error message and then terminate the program cleanly seem like they are much bigger than (say) in Python defining a custom exception, raising it, and then catching it in my main routine.

How do people do this sort of thing in Rust? Accept the need for big "add error handling" exercises once you've outgrown panics? Include a very basic error handling framework in their initial code? Design error handling up front as a key program structure decision? Leave panics in and use a custom panic handler to at least make them look a bit nicer?

I feel a little bit like I'm reinventing the wheel here, and there "must" be some sort of best practice in this area. Or am I being excessively optimistic? Or is there a way of "gradually" enhancing error handling - moving from a panic to a very general error mechanism, to something more refined as needed - that I'm missing?


I tend to actually "handle" the errors with the ? operator even in early drafts of my program. This is made simpler by crates like anyhow, which declares a "cover all" Error type (and an anyhow::Result shorthand).

So I tend to have full error propagation in an early draft. But for an end-user, an error propagating all the way out to main and terminating the program isn't that different from a panic, so the part I save for later is figuring out where and how to recover from some errors and where to use more specific error types to make recovering easier (or reporting nicer for the user).

In some cases, this means converting the the return type of a function from anyhow::Result<Something> to just Something, but the compile time type-checking makes it easy to fix all call sites, and it tends to be fewer changes this way the other way around.


I start with anyhow, and if I later decide to move to thiserror I start with an error enum that has one variant Anyhow(#[from] anyhow::Error) and gradually work from there, eventually removing anyhow.


Hmm, anyhow sounds useful, thanks. I've heard of it before, but never really looked into it for this purpose. I've not heard of thiserror, I'll take a look at that too.

Generally, thiserror is for libraries that need to report precise error causes and help users recover from the errors, and anyhow is a catch-all for applications or where you just don't care about the details besides displaying a message to the user.

For libraries I also like quick-error.


Like several other people have mentioned, I normally won't bother with unwrap() and just jump to returning a Result<(), Error>. Using the ? operator is actually easier than unwrapping, plus it opens the door for more a user-friendly experience out of the box.

I think anyhow has two killer features which make it stand out from the rest, though...

You can use the anyhow::Context extension trait to provide more information about the error. That means instead of "file not found", you might have an error that says "unable to parse the file, because we are unable to open foo.txt, because file not found".

The second killer feature is its Debug implementation. When you print an anyhow::Error using {:?} (which is what happens when you return Result<(), anyhow::Error> from main()) it will print out that aforementioned context chain alongside a backtrace when the RUST_BACKTRACE variable is set. For 99% of your functions you'll just be printing the error to the screen and exiting unsuccessfully anyway, so you get more by having a nice Debug implementation than you would if you had custom error types.

Here's a contrived example:

Contrived example
use anyhow::{Context, Error};
use std::{fs::File, io::Read, path::Path};

fn read_contents(filename: &Path) -> Result<String, Error> {
    let mut f = File::open(filename)
        .with_context(|| format!("Unable to open \"{}\"", filename.display()))?;

    let mut buffer = String::new();
    f.read_to_string(&mut buffer).context("Read failed")?;


fn parse_file(path: &Path) -> Result<(), Error> {
    let contents = read_contents(path).context("Unable to read the file")?;


fn main() -> Result<(), Error> {
    let path = Path::new("foo.txt");
    parse_file(path).context("Unable to parse the file")?;



And the error it prints out when backtraces are enabled:

Error: Unable to parse the file

Caused by:
    0: Unable to read the file
    1: Unable to open "foo.txt"
    2: No such file or directory (os error 2)

Stack backtrace:
   0: playground::main
   1: std::sys_common::backtrace::__rust_begin_short_backtrace
   2: std::rt::lang_start::{{closure}}
   3: core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once
             at /rustc/b41936b92cd8463020207cb2f62a4247942ef2e4/library/core/src/ops/
   4: std::panicking::try::do_call
             at /rustc/b41936b92cd8463020207cb2f62a4247942ef2e4/library/std/src/
   5: std::panicking::try
             at /rustc/b41936b92cd8463020207cb2f62a4247942ef2e4/library/std/src/
   6: std::panic::catch_unwind
             at /rustc/b41936b92cd8463020207cb2f62a4247942ef2e4/library/std/src/
   7: std::rt::lang_start_internal::{{closure}}
             at /rustc/b41936b92cd8463020207cb2f62a4247942ef2e4/library/std/src/
   8: std::panicking::try::do_call
             at /rustc/b41936b92cd8463020207cb2f62a4247942ef2e4/library/std/src/
   9: std::panicking::try
             at /rustc/b41936b92cd8463020207cb2f62a4247942ef2e4/library/std/src/
  10: std::panic::catch_unwind
             at /rustc/b41936b92cd8463020207cb2f62a4247942ef2e4/library/std/src/
  11: std::rt::lang_start_internal
             at /rustc/b41936b92cd8463020207cb2f62a4247942ef2e4/library/std/src/
  12: main
  13: __libc_start_main
  14: _start

(Emphasis mine)

When discussing error handling we often hear people speak of the "Happy path" and imply there is a "Sad Path". By "Happy Path" they are talking about the general flow of the programs logic when all is going well. The "Sad Path" being the messy details of what happens when things go wrong, that is to say error handling.

Typically there is a desire to make the Happy Path as clear as possible in the source code. It's the business logic, the algorithm, the whole purpose of the code after all. The happy path is what you are designing.

The desire then is to hide the Sad Path. Get all that messy error handling out of the way. Keep the Happy Path clear and readable. Ultimately this leads to totally ignoring the Sad Path by simply throwing an exception, often with some nebulous idea that the exception is handled elsewhere.

This is quite understandable and is naturally what we do when drafting some new solution to a problem.

However I have a little problem with this "clean Happy Path"/"hide the Sad Path" notion. Most of my work has been in embedded systems or server side processes. There it is very important to take care of every possible failure and react to it in some well planned manner. Things cannot just bomb out with an exception. Most errors are not exceptional anyway, they are a common occurrence, expected behaviour. Files can be missing, inputs can be wrong permanently or temporally, network connections fail, etc, etc, etc. Sometimes it seems every other line of code can be the source of a "sad" thing that needs dealing with appropriately.

The point I'm coming to is yes, error handling does make the job a lot harder as you say. That is because the Sad Path is in fact 50% of your design effort. It cannot just be brushed aside so easily.

Luckily Rust does not have exceptions so totally hiding error handling under the carpet while keeping the Happy Path clean is not so easy. Also luckily Rust offers very nice alternatives to exceptions. As we see in the suggestions above.

One just has to put the effort in. Because it's a required 50% of your problem.

All in all I'm not convinced that totally separating the Happy Path and the Sad Path is something we should strive for. The Sad Path is critical to the codes behaviour and should be more prominent in what the reader sees.


In the first draft of my programs, I use expect() with a message that tells me where the error occurred. I also return Result<Foo, std::error::Error>, where Foo is the return type, on those functions. Here's a snippet of some code I'm working on now,

pub async fn talk_to_client_async_mutex(
    data: Arc<Mutex<Data>>,
    client: &mut Child,
    msg: &[u8],
) -> Result<(), Box<dyn std::error::Error>> {
    let client_pid ="No PID for client");
    let mut to_client = client.stdin.take().expect("No stdin for client");
    let from_client = client.stdout.take().expect("No stdout for client");

That makes it easier to add proper error handling later. In some, but not all, cases I have to change the return type.


I think that's a pretty harsh interpretation of what I was saying. I absolutely don't want to ignore error handling - however, I'd prefer to separate the process of designing the core logic from the process of working out how to handle errors. That's precisely to allow me to give each of them the level of consideration they deserve. Having to think about error handling while I'm trying to work out the mainline logic is, in my opinion, a recipe for putting in over-simplistic error handling, precisely because you're trying to focus on the main thread of the logic, and so don't think the error logic through.

Furthermore, when I am designing the error handling, I'd like to abstract it out, so that the error handling code is clearly separated and can be reviewed and verified without the logic being obscured by unrelated details (in this case, the "mainline" code that handles non-error cases, or the "happy path" if you want to think of it like that).

:man_shrugging: I guess you can design your code lots of ways, and keeping error handling and core logic together is a valid approach. I prefer having errors clearly transfer the logic into dedicated code that's visibly doing the job of recovering and continuing (or aborting).

Note that I'm not arguing for exceptions in any of the above. Far from it - Rust's abstraction capabilities and explicit error types seem like they will make writing clean, robust error handling code far easier. Since working with Rust, I find myself very nervous writing Python code precisely because I can't be sure I've dealt with all of the potential error cases. But none of that means I want to try to design mainline and error handling code at the same time - both jobs are hard, and I want to focus on one at a time, that's all.


I think what we're seeing here is a difference in the core values/attitudes for different industries. In the embedded systems that @ZiCog is referring to you don't separate the error path from the main logic, because all the paths are core pieces of your application's logic.

You can imagine for something like a rover on Mars that the "sent telemetry back successfully" and "main antenna not responding" paths need equally as much thought put into them and it doesn't make sense to leave them separated or try to abstract out the error handling.


So let me start by saying that I'm still new to Rust, so I may not yet have quite caught on to all the idiomatic ways to do things (and I'm saying this in the best possible sense, it is good to learn a language's idioms!).

So what I do is twofold:

  • for libraries and library-like functions in binary crates, I very heavily rely on the ? operator as described above: almost all of my functions apart from the ones in files containing a main() function return a result
  • for top-level command-line handlers and, well, yeah, most of the functions in a file containing a main() function (not as a rule, but it usually works out this way), I use something like .expect(), but, hm, shameless plug: I actually use my own expect-exit crate; its interface has changed a bit since the first upload, but I think that it has pretty much matured now (of course, if it happens that this post brings a bit more attention to it and some brave people decide to try it and to point out some obvious problems, I'd only be too happy :))

In both cases, this allows me to do the equivalent of ... in Perl, raise NotImplementedError("oof") in Python, and so on, quite easily: for functions returning a result I place a Err("finish <function name> at some point"), possibly quoting a couple of the arguments to more easily figure out what I need to do when I decide to get to it, and for top-level functions I use a exit(...) expect-exit call the same way. This allows me to both test my error-handling routines (when I've placed an unconditional Err() in a library-like function - make sure the error is propagated and reported properly) and leave the actual implementation for later.

Hope this helps! Of course, as noted in the beginning, if any more seasoned Rustaceans would like to point out any problems with this approach, I'd only be too happy to learn more!

1 Like

Normally in Rust you'd use the todo!() macro with something like todo!("read the config file from the file system").

That way you don't need to make your function (and every function that calls it) return a Result, and it'll crash with a nice backtrace that says which function needs implementing.

I know it's off topic, but how often have you needed expect-exit now that you can return a Result<(), Error> directly from main() and get the same effect?

Well, I make most functions return a Result anyway, so that I can use ? inside :slight_smile: But, hm, thanks for introducing me to the todo!() macro, I hadn't really come across it.

And about the main() return value: mm, expect-exit was first written long after the 2018 edition. Please don't get me wrong, I'm not saying that returning a result from main() is not useful, it's just that I have trained myself for many years to make my programs produce human-friendly error messages, and I like the ability to customize them. I love the fact that main() can return a Result, it is great for many programs that really do not expect an error to occur, it is yet another way in which the Rust developers are helping create a world where programs produce understandable messages; it's just that it is not quite enough for my personal standards and habits, but that's just me :slight_smile:

1 Like

I think one of the things you are seeing here is the difference between dynamically typed languages (Python) and statically type (Rust). Usually in a dynamically typed language you can get away with changing return types with very little effort. Your program might break in one or two places but often you'll get away with it. In Rust changing the return type, not necessarily the error type, any return type, often involves refactoring a huge amount of your program just to get it to compile. This is just one of the things you have to accept with statically typed languages.


I think that's largely expected. My recollection of the discussion around the feature was that it didn't need to have customization hooks because it's expected that code that wants rigorous control over its output wouldn't use it anyway -- and it'd be easier to just write that code than to put it in a hook.

See also println!: very important for quick things, but the more rigorous a program gets the more likely that it never uses it, and instead uses writeln!.

1 Like

There's still a desire for improvement in this area.

That sounds like the right approach.

The standard library provides a nice default, but if people want to customise the way their program exits then it's better to give them full control than come up with some convoluted framework for printing error messages, backtraces, logging crashes to a 3rd party system, custom exit codes, etc.

I think this caused by your bad programming habits.

you are used to the freedom of python syntax, it's easy to change and no any cost.

but in rust, things are not easy as you wish.

Design the program architecture with paper first will help you with this problem.

a month ago, I am confused with this too

do not program first, try to design it first, and you will not lose in Error handle.

I'm not sure I follow this.

If one changes return types in any language that is going to break things. Those things need to be fixed. Surely that "huge amount of refactoring" is the same huge (or small) in pretty much any language, depending on how much breakage one has caused?

It's work to be done. Just hoping to "get away with it" is not acceptable in any language. Unless you really don't care if your creations work or not.

Rust helpfully points out the breakages ahead of running the code. Thus saving a lot of work later when things go belly up.

1 Like

I think it's a reasonable point, actually. When coding in Python, I think of the return type in terms of the non-error case. That's not language enforced, because Python isn't statically typed (apart from annotations and mypy), but it's how I design my functions. That doesn't mean that I ignore error cases, or "hope to get away with it", but it does mean that I treat errors as non-local control flow, and design my code with specific error handling locations, making sure that (1) any code that can raise an exception is covered by an error handler, and (2) each error handler has the information it needs to correctly handle the exceptions it receives.

With Rust's explicit error return types, the logic is different, because the error has to be threaded through the full call stack, from the point where the problem arose, to the point where it gets handled. That's not a problem, it's just different - and I need to change how I design functions as a consequence.

Overall, the resulting design is probably nicer (it means I never have any uncertainty about whether a given call can fail) but it does need extra boilerplate to pass the errors around - and that was basically the point of my original question, how do I minimise the (visual) impact of that extra control flow on my code in an idiomatic way. To which the answer seems to be to start with a very general error type like anyhow::Error and rely on the ? operator to "pass the error on" if you don't intend to handle it locally. Refining this to use more explicit error types, or handle errors closer to the source, is then less disruptive to the overall program logic.

Agreed, this is nice. When writing Python, I now find myself much more aware of the fact that I have to rely on documentation, testing, and common sense, to understand the potential error points in a program.

This depends on your definition of "belly up". In the types of program I write, unhandled exceptions need two main types of work to fix them. The first is to generate a more user-friendly error when exiting the program, so the user doesn't see a low-level traceback (this can sometimes be complex in practice, because you want the traceback for support/debugging purposes, but you don't want it "in the user's face"). The second is to make sure that resources are cleaned up as the program exits - Python handles this extremely nicely with context managers, I don't know yet what Rust's equivalent would be (although I'm sure there is one).

Neither of those issues is typically "a lot of work" as long as you have the basic framework for clean error handling in place.

PS What is the typical Rust idiom for guaranteed resource cleanup? Say, for example, I want to create a temporary directory and then do some work, and once I'm finished (whether in the normal control flow, or when an error occurs in called code resulting in an early exit with an error) I want to ensure that the directory and its contents are always deleted. In Python, I'd use a context manager. In C++ I gather that tends to be done using RAII (so I create a "temporary location" object on the stack which does the tidy up in its destructor). What's the Rust idiom for this?

1 Like