In rust, we don’t throw exceptions, we return Results. When coming from a language which throws exceptions, it can be easy to think of the Err type in a result as being just like an exception, but sometimes a little more thought can provide something more elegant.

In this example, I am looking at a simple function

fn add_user(username:&str, email:&str, password:&str) -> ??? 

For this function, we want to add a user to a system. The return type for this could take a number of forms.

First Attempt: Return a struct representing the user if we added the user, and an error if we didn’t

Result<User,Err>

This would be the goto function signature for me when starting out with rust. In my head (coming from Java) it maps nicely to a function signature along the lines of

public User add_user(String username, String email, String password)
	 throws SomeException, SomeOtherException {
...
}

So lets think about what those exceptional circumstances would be.

  • You might be trying to write to a database, and the connection fails. That’s an exceptional circumstance, but one we need to handle gracefully. You probably don’t want your real world app to crash when your database is unavailable briefly for some maintenance. You could internalise this in the function if this was a public API, so your return value in this case might be saying something like Something went wrong. This is probably temporary, so please try again in a bit, or in a function internal to a service, you might want to explicitly handle database errors (email to DBA, log something etc.).

  • The data you got might be no good. The username or email might already exist, the password might be blank, etc. Here you want a return value which informs the caller of what happened explicitly so that they can take corrective action within normal program flow.

Mixing these together in an Err return is inelegant for the caller to handle. They have to handle the Ok case and the other program flow cases, but will probably want to pass the exceptional cases back up the stack.

Better: Return an enum representing all events in normal program flow, and an Error for exceptions

Here we add an enum that looks something like this:

type Reason =  String;

enum AddUser {
	Added(User),
    	UsernameExists,
    	EmailExists,
    	UsernameInvalid(Reason),
    	EmailInvalid(Reason),
    	PaswordInvalid(Reason)
}

The function definition would now look like this:

fn add_user(username:&str, email:&str, password:&str) -> Result<AddUser,Err>

This looks much better. Err is reserved for genuine errors which we would likely propogate upwards, and our normal program flow is captured in one match statement. The calling function now looks something like this:

...

match add_user(name,email,pass)? {
	Added(u) => {...},
     	UsernameExists => {...}
    	...
}

...

The Err part can now be propogated unchanged up to a level where we can handle genuinely exceptional cases consistently.

Thanks for reading. If you have any questions, suggestions, or to point out any mistakes, please contact me at the email address below. I’d love to hear from you.