Rust Best Practices

Evolving FP Complete recommendations document. Can include anything from recommended libraries and tools to how to use language features. If you think it's a good idea, add it! We can worry about organizing it later.

Don't panic! Use errors

  • Custom app error type
  • Use ? a lot
  • Learn to use ok_err and map_err
  • Crates helping with error handling
    • anyhow can be useful for applications. It adds a lot of convenience to error handling and allows you to care less about specific error types.
    • thiserror can be useful for libraries. It is transparent to the public API as if you had written an implementation of std::error::Error.
  • Provide a user friendly Display for your errors

Context for errors

  • Don't simply package up an std::io::Error and call it a day
  • Provide some additional information like "Was trying to open file XXX and received this error".
  • Much more user friendly, much easier to debug

Be liberal in what you accept

  • Prefer taking &str over String in function arguments
  • Similarly, take &[T] instead of Vec<T>. If you just need to iterate over something, consider accepting a more general IntoIterator<Item = T>.
  • Avoids forcing someone to create an owned copy in many cases

Avoid references in structs

  • References in structs make you include lifetime parameters
  • Generally: avoid lifetime parameters if you can, simplifies code a lot
  • Usually, you'll want a String, not a &'a str, in your structs

Use an Rc or Arc when needed

  • Many borrow checker issues can be short-circuited by throwing Rc or Arc into things
  • Don't worry too much about optimizing these cases
  • Also, if it's not too expensive, consider a .clone() as well
  • A good example of not doing this and wasting a lot of time: https://www.fpcomplete.com/blog/avoiding-duplicating-strings-rust/

CLI: use clap

  • clap is a really nice library
  • Forces you to deal with "impossible" error cases
  • Less error handling in your own code
  • amber has a nice example of it

Lock your repos

  • Include Cargo.lock and a rust-toolchain
  • Makes it more reproducible
  • If you use Git deps, do it by commit SHA

Test your code

  • Obviously
  • Consider using quickcheck, it's nice!
  • For most unit tests, include within a #[cfg(test)] mod tests { ... } section.
  • For integration tests and larger unit tests, place in a separate .rs in the tests directory.

Lint your code

  • cargo clippy and cargo fmt are good
  • Include them in CI
  • Github Actions has nice stuff for that already

IDE

  • Use Rust Analyzer as the language server.
  • Visual Studio Code: the extensions rust-lang.rust and matklad.rust-analyzer are popular.

Logging

  • Use the tracing crate for logging in all libraries and applications.

Public vs private fields

From a discussion in an engineering meeting on June 11, 2025.

When declaring fields on a struct in Rust, you need to determine the visibility: public, private, or something in between (like pub(crate) or pub(in crate::mod_tree)). This applies to functions, methods, enums, and more as well, but the question comes up most often for structs: should the fields be public or private?

There will always be some room for variation and disagreement about this, and it's almost always a case-by-case basis. However, the following general guidelines work as good defaults.

  1. There's a big difference between public, published libraries and libraries that will be used internally by our team and/or customers. The guidelines here apply to the internal variety. We're punting on published library guidelines for now.
  2. If there are invariants to be maintained within a struct, such as ensuring that a field can never be 0, keep the field private. This is good encapsulation behavior.
  3. If the struct is mostly simple data (i.e., the Rust equivalent of a Plain Old Java Object), defaulting to public fields makes sense.
  4. More complex fields, such as Mutexs, should probably default to being more private.
  5. In general, using private fields can be a good way to protect consumers of a library from API breakage when internals change. However, when we're working in an internal capacity, it doesn't much matter if we break things in this way. Usually, the library author and application author are the same person, and regardless we're all on the same team anyway.