Rust resources

This page is a collection of resources, recommended crates, and tips for using the Rust programming language. I've included things that I have some first-hand experience with, except where otherwise noted. I'm just a "normal Rust user", and I've curated this in hopes that it may help others (and future me).

The Rust ecosystem moves fairly quickly, so this may not be as relevant after a couple of years. This page was created on 2025-04-10 and last updated on 2025-04-16.

Resources

Note: the Rust community has a lot of things called "books" that aren't necessarily the size of a printed book. I've tried to use scare quotes to distinguish books and "books".

Beginner Rust

  • See the official Learn Rust page.
  • Read The Rust Programming Language book, which is available for free.
  • Rustlings is a collection of beginner-level exercises with provided unit tests. I haven't done these, but the exercises look reasonable to me at first glance.

Advanced Rust

  • Rust Atomics and Locks book, which is available for free. I have only read bits of this.
  • Rust and WebAssembly "book". I think I referred to an earlier versions of this. It contains a some pointers to related crates and tools.
  • The Typestate Pattern in Rust: a blog post describing an API pattern for builder-type objects. You might encounter APIs like this in the wild. I don't need this pattern often, but it's good to be aware of.

References

  • std docs: the Rust standard library reference. You'll refer to this often, so set up a keyword search in your browser. Also available at https://docs.rs/std.
  • Rust Reference "book": more detailed syntax and semantics for the language. I refer to this occasionally to find obscure details. Most often, I seem to look up things related to attributes.

Updates

  • This Week in Rust is a weekly newsletter that aggregates other links.
  • The main Rust blog posts highlights of new versions. (Its posts are usually included in This Week in Rust.)

Community

  • Rust user forum: official forum. I land on this from searches sometimes but haven't personally lurked or posted here.

  • /r/rust: Reddit. Similarly, I land on this from searches sometimes but haven't personally lurked or posted here.

Similar pages

Recommended crates:

Style guides:

Collections of resources:

Setup

Security note

Installing software from the Internet runs other people's code on your computer, which gives that code access to your files, your network, and your compute resources. Even build systems and editors can run arbitrary code. Any cargo command should be treated as running the arbitrary code from that workspace (see Shnatsel blog post).

You won't be able to develop non-trivial software, Rust or otherwise, without running code you haven't reviewed, potentially from people or organizations you don't fully trust.

It's often a good idea to isolate your development environment by using a separate computer, virtual machine, container, user account, etc. If you run Linux, you might want to check out my Cubicle project, which sets up and runs development environments inside of isolated, disposable containers. I also have a post on how to run VMs on Linux.

Install

Use rustup, which manages the installation of Rust versions and components. This is the default installation method and works best for most cases. Rust releases every 6 weeks, so you'll want to be able to update versions over time (see the release train docs). I recommend the latest stable version of Rust, which is rustup's default, unless you have a specific need for a nightly, beta, or older build.

Link times during builds can be slow due to the default system linker. Rust defaults to GNU's linker on Linux, which performs especially poorly. If you're just starting out, it's OK to use the default linker and set this up later. The nightly compiler switched to using LLVM's lld in 2024-05 (see the official blog post), but the default hasn't been changed in stable yet. Another good option is mold, which is probably faster than lld. To switch linkers, see the linking section in The Rust Performance "Book" for lld or follow the instructions for mold. I've used mold for years and have only encountered problems with cross-compiling (and a colleague found a bug that mold's author fixed promptly).

Editor

See the official Tools page.

I use VS Codium with the official rust-lang.rust-analyzer extension, as well as tamasfe.even-better-toml for editing Cargo.toml files. VS Code has a page about using Rust with a bit of a tour. I find the inlay hints, which show type information alongside the code, to be very helpful.

I currently have the following Rust settings in my VS Codium config:

{
  "rust-analyzer.completion.autoimport.enable": false,
  "rust-analyzer.inlayHints.maxLength": 40,
  "rust-analyzer.checkOnSave.command": "clippy",
  "workbench.colorCustomizations": {
    "editorInlayHint.background": "#ffffff00",
    "editorInlayHint.foreground": "#ffffff50"
  }
}

Tools

cargo is Rust's build tool. Rust/Cargo comes with:

Cargo also has a concept of plugins that act as subcommands, where you can/should invoke tools named cargo-foo as cargo foo.

cargo install can install tools and cargo plugins (crates producing binaries) by building them from source. See cargo binstall next, which can download binaries instead.

cargo binstall

If you're using cargo install often or on a slow machine, you may not want to build these tools from source. cargo-binstall is a Cargo plugin that installs Cargo plugins. It tries to fetch official upstream binaries and falls back to building from source like cargo install.

To bootstrap this, you can install binstall from one of the binaries on their GitHub releases page, as I've done in this Cubicle package script. Alternatively, you can run cargo install cargo-binstall to build it from source.

By default, cargo binstall will also try to get third-party binaries from a quick-install artifact repository. You can disable this with:

export BINSTALL_STRATEGIES=crate-meta-data,compile

Security note: You might consider it riskier to download binaries from GitHub vs source code. You might find it even riskier to download binaries from a third-party artifact repository. Personally, I disable quick-install but am OK with getting binaries from upstream projects (within a sandboxed environment).

Ideally, crate authors would provide binaries using one of cargo binstall's standard naming conventions, or they would add crate metadata so that cargo binstall can find their binaries. For crates that offer binaries in non-standard locations, Cargo binstall allows specifying the metadata on the command line (see their README). I keep some workarounds.

cargo audit

cargo audit checks your project's transitive dependencies for known vulnerabilities. You should run this in CI.

cargo upgrade

cargo upgrade, which is part of cargo-edit is useful for upgrading your dependency requirements in Cargo.toml. It can do semver-compatible and semver-incompatible updates. See "Managing dependencies" below.

This functionality might make it into Cargo someday, but it hasn't yet.

rust-script

rust-script can run a .rs file as an executable (on Unix). The "script" specify dependencies in an inlined Cargo.toml within a header comment.

This functionality might make it into Cargo someday (see tracking issue), but it hasn't yet.

Crates

These dependencies are either de facto standards, commonly used, or I've used them enough to recommend them. This only includes things that many projects (at least many distributed systems projects) would want, and many projects will end up wanting other dependencies.

Error handling

  • anyhow: an opaque error type for programs that don't plan to handle or introspect the errors. Consider setting the non-default backtrace feature.

    One problem I've run into with anyhow is that it allows implicit conversions from std::io::Error, yet those messages are poor (for example, file not found but without a filename). I wrapped anyhow as somehow to work around this, which was tedious.

  • thiserror: macros to conveniently turn types into proper errors with messages. Callers shouldn't know that you've used thiserror vs writing error types manually. Use this in all libraries and in parts of programs where you do need to handle or inspect errors. This is a good, reasonable default to always use for all errors.

I've also seen snafu used, but I haven't used it personally. It seems to do what thiserror does, while also allowing errors to be wrapped with more context.

Network

  • futures: basic operations on futures, such as waiting on multiple things concurrently.
  • hyper: HTTP server library. Technically this also includes an HTTP client library, but I'd recommend the higher-level reqwest. The hyper ecosystem has seen a lot of churn, unfortunately, and it hasn't been easy to use historically.
  • reqwest: HTTP client, with blocking and async variants. Built on hyper.
  • rustls: TLS library supporting different cryptography backends.
  • tokio: async runtime, useful for networking as well as filesystems, signals, etc. Rust has a few different async runtimes, but this is the major one that most projects use. Tokio has an ecosystem of compatible crates. Strongly consider the rt-multi-thread feature, which enables the multi-threaded scheduler.
  • tonic: gRPC implementation. Works with prost for Protocol Buffers by default.

Parsing/serialization

  • clap: parses command-line arguments. I always use this with derive macros, which requires the derive feature. Also consider setting the wrap_help feature. Clap releases very frequently but is good about maintaining compatibility. Clap prints brief help with -h and the full help with --help by default, so many Rust programs inherit this behavior.
  • humantime: parses durations (like "10 minutes 4 seconds"). Unfortunately, its syntax doesn't support fractional decimals like "3.5 hours" (see issue). Alternatively, consider fundu for more control over parsing, but I haven't used it.
  • prost: Protocol Buffers code generator and runtime library. Used in tonic for gRPC by default.
  • regex: regular expressions.
  • serde: a serialization/deserialization framework. This is widely used for types/formats that know how to parse themselves. The framework doesn't quite fit for something like Protocol Buffers that requires separate schema information to interpret.
  • serde-json: serde parser for JSON.
  • url: URL parsing.

Testing

  • expect-test: a basic snapshot testing library. Run UPDATE_EXPECT=1 cargo test to update snapshots, either in inline strings or separate files. When a test fails, I usually update the snapshots and use version control to see what changed (committing or staging my code changes beforehand).
  • indoc: a macro to unindent multi-line strings. This can be helpful when testing snapshots.
  • tempfile: useful for creating and cleaning up temporary files/directories for tests.

insta is a heavyweight snapshot testing crate and tool. I prefer the simplicity of expect-test.

cargo-nextest is a Cargo plugin that provides a different test runner with better parallelism. It runs each test in its own process, while cargo test runs the tests for a given crate in the same process. Therefore, you can end up with tests that work under cargo nextest but not cargo test (and theoretically vice versa). I recommend defaulting to cargo test until/unless you have issues with test parallelism.

Misc

  • chrono: calendar library to deal with dates and time zones. Alternatively, see jiff below.
  • jiff: another calendar library to deal with dates and time zones. I haven't used this, but I'd consider it for new projects. See its comparison vs chrono and others.
  • log: logging API, including structured logging. Everything should write log messages to this (directly or indirectly). This doesn't provide an implementation for where the messages go (they're discarded by default); there are many options for that.
  • rand: random numbers.
  • sha2: SHA-2 family of hash functions, including SHA-256. Other cryptography crates by RustCrypto tend to be good choices.

Managing dependencies

You can add dependencies with cargo add. Use --dry-run first and review the compile-time features. Features are often explained in the crate's Rust-level docs, in its README, or in comments in the features section of its Cargo.toml.

Use cargo tree to review transitive dependencies.

You probably want to put the Cargo.lock file in version control for team projects. It helps CI and developers on the project use identical dependencies, even though users of the project may use something different. See the Cargo FAQ entry.

To update dependencies, use cargo update to update the pinned versions in Cargo.lock. Use cargo upgrade [--incompatible] from cargo-edit to update the version requirements in Cargo.toml.

Declare all your dependencies at the root of the workspace, even if your project has multiple crates. The individual crates can inherit the workspace version and features (see docs). This makes it easier to review and update dependencies and versions, and it encourages different developers in the same workspace to standardize on the same set of crates.

Other best practices

Rust versions

Set your MSRV (minimum supported Rust version) in Cargo.toml, like this:

[package]
rust-version = 1.86

This simplifies error messages and communication if other people have an unsupported Rust version installed.

Also, pin a Rust version for CI. Otherwise, when a new Rust release occurs, you might get different warnings (mostly from Clippy).

Clippy lints

These are some pedantic Clippy lints that I recommend enabling in Cargo.toml. It's easy to disable individual lints later if you don't end up liking them.

[lints.clippy]
explicit_into_iter_loop = "warn"
explicit_iter_loop = "warn"
implicit_clone = "warn"
redundant_else = "warn"
try_err = "warn"
unreadable_literal = "warn"

Primitive casts

Use From and TryFrom for casts when possible:

assert_eq!(3, u64::from(3u32)); // can't fail
assert_eq!(3, u32::try_from(3u64).expect("TODO")); // could fail

TryFrom forces the code to handle the case or to panic when the cast isn't lossless.

One annoying thing is that converting from usize to u64 isn't allowed with From (it requires TryFrom). usize is 32 bits on 32-bit architectures and 64 bits on 64-bit architectures, so it'd be fine on today's computers. I guess usize could hypothetically be 128 bits in some future CPU, but From<usize> isn't even implemented for u128! The future may bring even larger addresses, I guess. It seems fair to panic in these cases, at least in programs that don't need to be portable to hypothetical CPUs:

use std::mem::size_of;

const fn u64_from_usize(x: usize) -> u64 {
    const _: () = assert!(
        size_of::<usize>() <= size_of::<u64>(),
        "expected <= 64-bit CPU"
    );
    x as u64
}

assert_eq!(3u64, u64_from_usize(3usize));

Avoid using the as keyword for casts when possible, since it can be dangerous:

  • Casting from a larger to a smaller integer truncates,
  • Casting from larger to a smaller float is lossy,
  • Casting from a float to an integer rounds,
  • Casting from an integer to a float is lossy, and
  • Casting from a char to an integer casts its code point and then may have the issues above.

For float to integer conversions, as is required. Use floor, round, trunc, etc to make the loss in precision explicit, and consider isolating the conversion in its own function.

Gotchas

starts_with/ends_with on str vs Path

str::starts_with and Path::starts_with sound the same but have different semantics:

  • /etc/passwd as a str starts with /etc and /e.
  • /etc/passwd as a Path starts with /etc but not /e.

It's easy to mix these up. The ends_with methods are analogous.

let _ vs let _name

A single underscore does not create a binding:

let _ = x.lock();

So the result gets dropped immediately.

On the other hand:

let _guard = x.lock();

This creates a binding with a name that starts with an underscore, so the result gets dropped when that binding goes out of scope. The underscore here means don't warn about this name being unused.

See https://github.com/rust-lang/rust/issues/40096 for one of several issues filed on this.

This isn't a meaningful difference most of the time, and the compiler has some very good error messages when it catches this. Try it in the Rust playground.

Misc

Stable as nightly

If you want your stable compiler to pretend to be a nightly compiler, set the environment variable RUSTC_BOOTSTRAP=1. Because of the release train, it will behave similarly to a nightly build, approximately 12 weeks prior to the stable release. This feature is documented but is fairly hidden. Hopefully you won't need it. I found it useful when I needed a nightly version to build std for a weird CPU, but I otherwise had available and wanted to use stable Rust.

Unprocessed

I haven't looked through this stuff in detail or gotten a chance to try it yet, but it may be useful to include above:

Crates

  • cov-mark: looks like a good way for unit tests to make sure they're covering intended blocks.
  • icu: for localization/internationalization. I haven't needed this yet.
  • proptest: property-based testing. I've used this some but not enough to recommend it over alternatives yet.

Other