Until now, we've dealt with the Result
type by simply calling unwrap
. This will panic!
and cause our program to crash anytime the Result
is of the Err
variant. Let's look a little more closely at what the Result
type actually is.
pub enum Result<T, E> {
Ok(T),
Err(E),
}
source: https://doc.rust-lang.org/std/result
You may not be familiar with this as we haven't really used it in practice just yet, but this is an Enum
type and not a Struct
type that you're now familiar with.
An Enum
is different from a Struct
. A Struct
groups together related fields and data. An Enum
, on the other hand, presents a set of possible values of which only one is used at a time. In the case of the Result
type, there is an Ok
variant and an Err
variant. Each variant wraps a value.
We typically access the Enum
variant values with a match
statement or with the help of methods available on the particular Enum
. Like Structs
, Enums
also have methods which are defined in an impl
block.
For example, if we look at the unwrap
method, this actually returns the value wrapped in the Ok
variant. However, if the variant returned is of the Err
type, the program will panic and print the &str
wrapped in that variant.
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.unwrap(), 2);
...
let x: Result<u32, &str> = Err("emergency failure");
x.unwrap(); // panics with `emergency failure`
source: https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap
So what is a better way to handle errors in our program? What we really want is for our functions to return Result
types and then allow the functions calling them to better handle those Result
variants and return a friendly error message to the user instead of crashing the program.
Let's make some changes to our program. The first thing we're going to do is turn our program into a library. The main
function in main.rs
is the only function that does not support a return type. The purpose of the main
function is to be the entrypoint into our program when we run cargo run
. So what we want is for our main.rs
to call our library and treat it as an external crate. This is not unlike the way we import external crates by adding them to our Cargo.toml
and bring them into scope with use
statements.
For additional reading on understanding the differences between lib.rs
and main.rs
I found this Stack Overflow question to be helpful.
So let's start by creating a lib.rs
and move all of our program logic into there.
$ tree
.
├── Cargo.lock
├── Cargo.toml
└── src
├── lib.rs
├── main.rs
└── transaction.rs
2 directories, 5 files
We'll rename the main
function to run
and this will return a Result
enum. All other functions and statements will be the same except they are now moved into lib.rs
.
Here is the modified run
function signature along with the last line.
lib.rs
pub fn run(raw_transaction_hex: String) -> Result<String, dyn Error> {
let transaction_bytes = hex::decode(raw_transaction_hex)?;
...
let json = serde_json::to_string_pretty(&transaction)?;
Ok(json)
}
Let's break this down a bit:
- We've added
pub
at the front to ensure that this function is callable from the outside, such as from ourmain.rs
file. This is the public entrypoint into our library. - We're now taking the raw transaction hex string as an argument.
- The function return type is a
Result
enum. TheOk
variant must be aString
type. - The
Err
variant is of the typedyn Error
. Thedyn Error
keyword indicates a trait object. What this means is that the expected return type is any type that implements the stdError
trait. - Instead of calling
unwrap
on thehex::decode
andto_string_pretty
methods, we are now appending the question mark operator,?
. This does something similar tounwrap
in that it returns the value wrapped in theOk
variant. However, in the case of an error it will return theErr
variant instead of panicking. In other words, in the case of an error, the function execution will stop and therun
method will return thisErr
variant for ourResult
enum. - We wrap the the
json
string inside anOk
variant to match our function's return type. Remember, it is expecting anOk
or anErr
variant with their wrapped values of specific types. We can't simply return theString
type.
Let's update our main.rs
file which is now currently empty:
fn main() {
let raw_transaction = "010000000242d5c1d6f7308bbe95c0f6e1301dd73a8da77d2155b0773bc297ac47f9cd7380010000006a4730440220771361aae55e84496b9e7b06e0a53dd122a1425f85840af7a52b20fa329816070220221dd92132e82ef9c133cb1a106b64893892a11acf2cfa1adb7698dcdc02f01b0121030077be25dc482e7f4abad60115416881fe4ef98af33c924cd8b20ca4e57e8bd5feffffff75c87cc5f3150eefc1c04c0246e7e0b370e64b17d6226c44b333a6f4ca14b49c000000006b483045022100e0d85fece671d367c8d442a96230954cdda4b9cf95e9edc763616d05d93e944302202330d520408d909575c5f6976cc405b3042673b601f4f2140b2e4d447e671c47012103c43afccd37aae7107f5a43f5b7b223d034e7583b77c8cd1084d86895a7341abffeffffff02ebb10f00000000001976a9144ef88a0b04e3ad6d1888da4be260d6735e0d308488ac508c1e000000000017a91476c0c8f2fc403c5edaea365f6a284317b9cdf7258700000000";
match transaction_decoder::run(raw_transaction.to_string()) {
Ok(json) => println!("{}", json),
Err(e) => eprintln!("{}", e),
}
}
Let's review the changes:
- The name of our library is
transaction_decoder
. This can be found in ourCargo.toml
which is the same as thepackage_name
, which might be different for you. We're calling therun
method on our library and passing in theraw_transaction
String
. We have to convert the text in quotes to aString
type with theto_string
method. Remember, whenever we write text in quotes, Rust interprets as the&str
type. - We use the
match
pattern to handle theResult
. Note the use ofeprintln!
which prints toio::stderr
instead ofio::stdout
.
Let's attempt to build and run our program now with cargo run
.
If you did this correctly, you should get one particular compiler error:
error[E0277]: the size for values of type `(dyn StdError + 'static)` cannot be known at compilation time
--> src/lib.rs:84:44
|
84 | pub fn run(raw_transaction_hex: String) -> Result<String, dyn Error> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `(dyn StdError + 'static)`
note: required by a bound in `Result`
--> /Users/shaanbatra/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/result.rs:502:20
|
502 | pub enum Result<T, E> {
| ^ required by this bound in `Result`
The dyn Error
doesn't have a known size at compile time. Let's understand this a bit.
Our run
function might return many different types of errors. For example, if hex::decode
fails, it returns an enum, FromHexError
. If serde_json::to_string_pretty
fails, it returns a struct, serde_json::Error
.
Rust doesn't know which one and therefore doesn't know the size at compile time for the return type. Whenever we don't know the size of something at compile time, we need to allocate data to the heap and return a pointer reference. We can use Box
to do this. Very simply, Box
allocates data on the heap and then returns a pointer reference. By setting the return error type as a Box
, all of our different errors in the code block will be implicitly wrapped in Box
as well. This way, all of the different error types will have the same size since they will all be pointers. They still point to different places in memory and so Rust will determine how to handle them at runtime instead of compile time.
So let's fix this and modify the function signature by Box
ing our errors: pub fn run(raw_transaction_hex: String) -> Result<String, Box<dyn Error>>
.
If we run cargo run
now, everything should work and print the same result! Great!
Let's test an error case. In our main.rs
, we'll comment out the original transaction hex and replace it with the letters "abc". This should fail to hex::decode
because it has an odd length (remember, every two hex characters is 1 byte).
fn main() {
// let raw_transaction = "010000000242d5c1d6f7308bbe95c0f6e1301dd73a8da77d2155b0773bc297ac47f9cd7380010000006a4730440220771361aae55e84496b9e7b06e0a53dd122a1425f85840af7a52b20fa329816070220221dd92132e82ef9c133cb1a106b64893892a11acf2cfa1adb7698dcdc02f01b0121030077be25dc482e7f4abad60115416881fe4ef98af33c924cd8b20ca4e57e8bd5feffffff75c87cc5f3150eefc1c04c0246e7e0b370e64b17d6226c44b333a6f4ca14b49c000000006b483045022100e0d85fece671d367c8d442a96230954cdda4b9cf95e9edc763616d05d93e944302202330d520408d909575c5f6976cc405b3042673b601f4f2140b2e4d447e671c47012103c43afccd37aae7107f5a43f5b7b223d034e7583b77c8cd1084d86895a7341abffeffffff02ebb10f00000000001976a9144ef88a0b04e3ad6d1888da4be260d6735e0d308488ac508c1e000000000017a91476c0c8f2fc403c5edaea365f6a284317b9cdf7258700000000";
let raw_transaction = "abc";
match transaction_decoder::run(raw_transaction.to_string()) {
Ok(json) => println!("{}", json),
Err(e) => eprintln!("{}", e),
}
}
Let's see what happens if we do that.
We'll get the following error printed to the terminal.
Odd number of digits
First of all, this is a nicer error message. It's not a harsh program crash with unnecessary information about our code printed out. We do get an error here that makes sense, but there's not a lot of context is there? Clearly, our hex::decode
method returned the Err
result, but it would be nice to add some more context to this error message. We can do that with the help of the map_err
method, which is available on the Result
enum.
lib.rs
...
let transaction_bytes = hex::decode(raw_transaction_hex).map_err(|e| format!("Hex decoding error: {}", e))?;
...
The map_err
method takes a closure which passes the error message as an argument. We then return a String
which modifies the error message by adding some additional text in front. You'll notice that format!
works similarly to println!
in that it takes a formatting string which replaces the brackets {}
with the arguments that follow. However, instead of printing, it simply returns a String
.
Let's run cargo run
again and see how this looks:
Hex decoding error: Odd number of digits
Pretty neat! Alright, let's finish up our error handling by returning a Result
for any function that might panic!
. All we have to do is modify our function signatures and replace any unwrap
calls with a ?
. Then, anywhere those functions are called, we need to handle them with the ?
operator as well. This way, all errors will essentially bubble up to the user and won't cause our program to crash.
Why don't you go ahead and make those changes? Check out the code
folder of this course to compare your changes to mine. Make sure to update the unit tests as well! Hint: You don't need to use Box<dyn Error>
for every function. Some of them will return only one type of error and that can be determined at compile time.
Next up, let's look at command line arguments and see how we can accept any input from the terminal. We no longer have to hardcode our transaction example in the code!