How XV uses Panics
XV is a terminal hex viewer that I am working on. It is the first “real” Rust project that I am working on, coming from a Java background.
Java has exceptions. Both checked exceptions, identified by having the Exception class as a parent class, and unchecked exceptions, which have RuntimeException as a parent class.
Rust does not have exceptions. Rust has panics, which, depending on build-time configurations, are either catch-able when they unwind the stack, or only produce a backtrace, or just immediately aborts the process. This is controlled by the “panic” setting in the “profile” sections of your Cargo.toml file.
Error handling in Rust is mostly done via sum-types. That is, types whose values can have one of an enumerated number of forms. For instance, Option values can either Some(value), or None. People sometimes call these algebraic data types, which is correct though that term also include product-types, which might be better known as tuples. Many Rust methods and functions return their values wrapped in a sum-type that can indicate the successful execution and the result, or an error value that may contain some additional details of what went wrong.
In Java, the unchecked runtime exceptions are, at least in principle, meant to indicate the presence of bugs. If your program get a runtime exception, it should be because there is a bug in your code. Checked exceptions indicate exceptional situations that a program is expected to deal with, such as not having the right permissions to open a file — problems that can reasonably occur, and whose causes are perhaps outside of the control of the program itself.
My instinct is that panics in Rust are similar to runtime exceptions in that they indicate the presence of bugs. And the use of sum-types is equivalent to checked exceptions.
For this reason, there is a liberal use of the “unwrap” methods in the XV code base. The “unwrap” method returns the wrapped result of a successful operation, or panics if the operation was not successful. I have done this in cases where the operations are expected to always be successful. Which means that if they are not, then we likely have a bug on our hands.
The most common cases are casts, and unwrapping options. For safe casts I use the From trait, and for unsafe casts I use TryFrom and unwrap. Cursive, the terminal UI framework that XV uses, also has a number of APIs where Option is returned — particularly when working with “user data”, or when getting interface components back out from the Cursive instance. Sometimes I use if-let clauses, and sometimes I unwrap, depending on whether or not I’m expecting to always see a Some() result.
Arithmetic overflow — when the result of an arithmetic operation cannot be represented by the number type of the operands — can also result in panics. By default, arithmetic overflows will panic in debug builds, and silently produce wrong results in release builds. In XV, I have decided that the slight increase in size of the binary, is a fair price to pay for also having overflow checks enabled in release builds. You can do this with the “overflow-checks = true” setting under the “profile.release” section in your Cargo.toml file. I have chosen the number types that are used in the XV code base, such that overflows should not happen. But if they do, then XV will panic to indicate the bug.
XV has a custom panic handler, similar to Human Panic, which captures the backtrace, the error itself, and other useful information, if a panic occurs. When XV is started up again after a crash, it will also show an error message that tells people to file a bug report on GitHub.
So the short version is that XV uses panics to signal the presence of bugs, and a mixture of code idioms and build settings ensure that I cast a wide net. And should they happen, then my custom panic handler will help ensure that the bugs are not only filed, but also contain enough information that they can be acted upon. At least I hope so.