Skip to content

Best Practices for Rust Projects

Info

We use Rust, and in general, strongly typed languages, to make undesirable states difficult or impossible to represent.

Speed up the Inner Development Loop

Use the lld linker for faster linking, and leverage cargo-watch to launch tests and builds when files change, thereby improving perceived compilation time. For more details see Zero to Production: Setup - Toolchain, IDEs, CI.

Use Test Driven Development

This is absolutely crucial. See Using Tests and Mocks for a primer on Rust testing.

Make it Work, then Make it Better

Often it's best to create solutions that emphasize expressiveness, testability, and maintainability, rather than hyper-optimize early in the development, particularly when working with a team of developers. If a performance bottleneck is located, these can be fixed later.

Keep Things Modular

Modularity improves the code's readability, testability, and simplifies error handling. Keep the following points in mind when developing a program.

  • Each function should deal with a single task. If a function is handling multiple responsibilities then it should likely be refactored.
  • Use configuration variables to reduce the number of variables that need to be in scope.
  • Handle error messages correctly the first time. Using expect statements is fine for prototyping, but once an implementation is chosen, handle errors explicitly so that large code overhauls are avoided.
  • Place error-handling code in one place (as much as possible).

Separation of Concerns for Binary Projects

The Rust community has developed standards relating to what should be included in main.rs and what should be included in lib.rs. Primarily, only command line parsing, configuration setup, and a call to a run function in lib.rs (with associated error handling) should ever be inside the main function. Prototype code is the obvious exception to this rule, yet, it's always a good idea to keep these practices in mind as the code is developed.

It is also best practice to group related program configuration variables into a struct Config which is parsed in main by calling its constructor (e.g. new).

Note: Since the main function isn't testable directly, removing as much logic from main as possible improves the overall testability of the program.

Handle Potential Errors with a Result

Returning a Result ensures that any errors possible in a function are handled when the function is called. Use ? to propagate errors upwards, and couple this with crates like thiserror or anyhow for truly effective error handling.

Split Code into a Library Crate

Move everything that isn't in the main function into src/lib.rs. This includes:

  • All functions called in main
  • Relevant use statements
  • Configuration structs and method definitions

To make objects available elsewhere in the same crate, first use the pub keyword to make them public. Anything in the src/lib.rs file will be available under the crate's name (from Cargo.toml) in src/main.rs any by the crate::<object> module in other modules included in src/lib.rs any of the objects can be used by prefixing the crate's name, or by including a use <crate_name>::<object>; call to bring the <object> into scope.

Trade-Offs of Using clone

Using clone has runtime costs. For this reason the Rust community disfavours the its use to satisfy the borrow checker. However, it isn't always a bad idea to use clone. When the data being cloned is very small and when prototyping code it is acceptable to use clone to help address ownership problems. With more experience, better ways of satisfying the borrow checker will become clear, at which point you can hyper-optimize the code if you please.