---
title: "Rust Development"
description: "This is a document that contains a bunch of useful resources for getting started with Rust and adhering to our coding principles."
url: https://develop.sentry.dev/engineering-practices/rust/
---

# Rust Development

## [Getting Started](https://develop.sentry.dev/engineering-practices/rust.md#getting-started)

* A quick introduction into the Syntax for first-timers: [A half-hour to learn Rust](https://fasterthanli.me/articles/a-half-hour-to-learn-rust)
* The Rust Book, comprehensively documenting the language: [The Rust Programming Language](https://doc.rust-lang.org/book/)
* [The Async Book](https://rust-lang.github.io/async-book/)
* Sentry employees: Join the `#discuss-rust` channel in Slack

## [Coding Principles](https://develop.sentry.dev/engineering-practices/rust.md#coding-principles)

### [Iterators](https://develop.sentry.dev/engineering-practices/rust.md#iterators)

Prefer explicit iterator types over `impl Iterator` in stable, public interfaces of published crates. This allows to name the type in places like associated types or globals. The type name should end with `Iter` per naming convention.

In addition to the standard `Iterator` trait, always consider implementing additional traits from `std::iter`:

* `FusedIterator` in all cases unless there is a strong reason not to
* `DoubleEndedIterator` if reverse iteration is possible
* `ExactSizeIterator` if the size is known beforehand

If it is exceptionally hard to write a custom iterator, then it can also be a private [newtype](https://doc.rust-lang.org/1.0.0/style/features/types/newtype.html) around a boxed iterator:

```rust
pub struct FooIter(Box<dyn Iterator<Item = Foo>>);

impl Iterator for FooIter {
    type Item = Foo;

    fn next(&mut self) -> Option<Self::Item> {
        self.0.next()
    }
}
```

### [Async Traits](https://develop.sentry.dev/engineering-practices/rust.md#async-traits)

Support for async in **traits** has [landed in Rust](https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html) and should generally be preferred now.

```rust
pub trait Database {
    fn get_user(&self) -> impl Future<Output = User> + Send;
}

impl Database for MyDatabase {
    async fn get_user(&self) -> User {
        todo!()
    }
}
```

Note that the returned future type is `Send`, to ensure that it can run on a multi-threaded runtime.

When you need dynamic dispatch or have to support Rust versions older than 1.75 consider using the [`async-trait`](https://docs.rs/async-trait/) crate.

### [Avoid `.unwrap()`](https://develop.sentry.dev/engineering-practices/rust.md#avoid-unwrap)

This may seem obvious, but *almost always* avoid the use of `.unwrap()`. Even if a piece of code is guaranteed not to panic in its current form (because some precondition is met), it might be reused or refactored in a future situation where the precondition does not hold anymore.

Instead, refactor your code and function signatures to use `match`, etc.

### [Use `.get()` Instead of Slice Syntax](https://develop.sentry.dev/engineering-practices/rust.md#use-get-instead-of-slice-syntax)

Slice syntax (`&foo[a..b]`) will panic if the indices are out of bounds or out of order. Especially when dealing with untrusted input, it is better to use `.get(a..b)` and an `if let Some(...) =` expression.

### [Checked Math](https://develop.sentry.dev/engineering-practices/rust.md#checked-math)

Arithmetic under/overflows will panic in debug builds, but will lead to wrong results or panics in other parts of the code (like slice syntax mentioned above) in release builds. It is a good idea to always use checked math, such as `checked_sub` or `saturating_sub`. The `saturating` variants will be capped at the appropriate `MIN/MAX` of the underlying data type.

### [Field Visibility](https://develop.sentry.dev/engineering-practices/rust.md#field-visibility)

By default, fields of structs (including tuple-structs) should be fully private. The only exception to this are:

* Newtypes (1-tuple structs) where direct access to the inner type is desired. This is to annotate semantics of a type where accessing the inner type is required.
* Plain data types with a very stable signature. For example, schema definitions such as the Sentry `Event` protocol.

Mixed visibility is never allowed, not even between private and `pub(crate)` or `pub(super)`. Instead, provide accessors:

* `foo()`, `foo_mut()` and `set_foo()` for exposing the values
* `pub(crate)` or `pub(super)` accessor functions in corner cases
* A `Default` implementation and builder when incremental construction is required

## [Style Guidelines](https://develop.sentry.dev/engineering-practices/rust.md#style-guidelines)

In general, we follow the official Rust [Style Guidelines](https://doc.rust-lang.org/1.0.0/style/README.html).

### [Import Order](https://develop.sentry.dev/engineering-practices/rust.md#import-order)

Imports must be declared before any other code in the file, and can only be preceded by module doc comments and module attributes (`#![...]`). We bundle imports in groups based on their origin, each separated by an empty line. The order of imports is:

1. Rust standard library, comprising `std`, `core` and `alloc`
2. External & workspace dependencies, including `pub use`
3. Crate modules, comprising `self`, `super`, and `crate`

This is equivalent to the following `rustfmt` configuration, which requires nightly:

```toml
imports_granularity = "Module"
group_imports = "StdExternalCrate"  # nightly only
```

**Example:**

```rust
use std::borrow::Cow;
use std::collections::HashMap;
use std::io::{self, Seek, Write};

use fnv::{FnvHashMap, FnvHashSet};
use num::FromPrimitive;
use symbolic_common::{Arch, DebugId, Language};
use symbolic_debuginfo::{DebugSession, Function, LineInfo};

use crate::error::{SymCacheError, SymCacheErrorKind, ValueKind};
use crate::format;

pub use gimli::RunTimeEndian as Endian;
```

### [Declaration Order](https://develop.sentry.dev/engineering-practices/rust.md#declaration-order)

Within a file, the order of components should always be roughly the same:

1. Module-level documentation
2. Imports
3. Public re-exports
4. Modules and public modules
5. Constants
6. Error types
7. All other functions and structs
8. Unit tests

There is no hard rule on declaration order, but we suggest to place the more significant item first. For example, for types that expose an iterator, declare the type and its impl block (including `fn iter`) first, then below define the corresponding iterator.

When declaring a structure with an implementation, always ensure that the struct and its impl blocks are consecutive. Use the following order for the blocks:

1. The `struct` or `enum` definition
2. `impl` block
3. `impl` block with further constraints
4. Trait implementations of `std` traits
5. Trait implementations of other traits
6. Trait implementations of own traits

Inside an `impl` block, follow roughly this order (public before private):

1. Associated constants
2. Associated non-instance functions
3. Constructors
4. Getters / setters
5. Anything else

### [Default Lints](https://develop.sentry.dev/engineering-practices/rust.md#default-lints)

We use [clippy](https://github.com/rust-lang/rust-clippy) with the `clippy::all` preset for linting. See The *Configuring CI* section for how to invoke it.

In addition to that, every crate enable the following warnings in its top-level file after the doc block before imports (see the [list of all available lints](https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html)):

```rust
#![warn(missing_docs)]
#![warn(missing_debug_implementations)]
```

Note that we do not enable `unsafe_code`, since that should already stand out in code review. There are a few legitimate cases for unsafe code, and the `unsafe` keyword alone already marks them sufficiently.

### [Naming](https://develop.sentry.dev/engineering-practices/rust.md#naming)

We are strictly following the [Rust Naming Conventions](https://doc.rust-lang.org/1.0.0/style/style/naming/README.html). The entire conventions apply, although here are a few highlighted sections:

* [Avoid redundant prefixes](https://doc.rust-lang.org/1.0.0/style/style/naming/README.html#avoid-redundant-prefixes-%5Brfc-356%5D)
* [Getter and setter methods](https://doc.rust-lang.org/1.0.0/style/style/naming/README.html#getter/setter-methods-%5Brfc-344%5D) (`foo` instead of `get_foo`)
* [Conversions](https://doc.rust-lang.org/1.0.0/style/style/naming/conversions.html) (`as_foo` vs `to_foo` vs `into_foo`)
* [Iterators](https://doc.rust-lang.org/1.0.0/style/style/naming/iterators.html)
* [Constructors](https://rust-lang.github.io/api-guidelines/predictability.html#constructors-are-static-inherent-methods-c-ctor) (Structs generally have `fn new(...) -> Self`, which never returns Result)

### [Writing Doc Comments](https://develop.sentry.dev/engineering-practices/rust.md#writing-doc-comments)

We follow conventions from RFC 505 and RFC 1574 for writing doc comments. See the [full conventions text](https://github.com/rust-lang/rfcs/blob/master/text/1574-more-api-documentation-conventions.md#appendix-a-full-conventions-text). Particularly, this includes:

* A single-line short summary sentence written in American English using third-person.
* Use the default headers where applicable, but avoid custom sections outside of module-level docs.
* Link between types and methods where possible, especially within the crate.
* Write doc tests

### [Writing Doc Tests](https://develop.sentry.dev/engineering-practices/rust.md#writing-doc-tests)

For crate-public utilities and SDKs, prefer writing at least one doctest for the critical path of your methods or structs. Of course, this requires the interface to be public. This should even take precedence over an equal unit test, since it both tests and documents the API.

To format code in doc comments, you can temporarily [configure rustfmt](https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#format_code_in_doc_comments) to do so. We might add this to our default configuration at some point:

```rust
format_code_in_doc_comments = true
```

### [Writing Tests](https://develop.sentry.dev/engineering-practices/rust.md#writing-tests)

All test functions should contain the function name + a simple condition in their name. The test name should be as concise as possible and avoid redundant words (such as "should", "that"). Examples:

* `tests::parse_empty`
* `tests::parse_null`

Place unit tests in a `tests` submodule annotated with `#[cfg(test)]`. This way, imports and helper functions for tests are only compiled before running tests. Depending on the editor, `rust-analyzer` will auto-expand `tmod` to an appropriate snippet.

```rust
fn foo() {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn foo_works() { .. }
}
```

Integration tests should go into the `tests/` folder, ideally into a file based on the functionality they test. Such tests can only use the public interface. See [this blog post](https://matklad.github.io/2021/02/27/delete-cargo-integration-tests.html#Rules-of-Thumb) for tips on how to structure integration tests.

For libraries, consider providing examples in `examples/`.

## [Development Environment](https://develop.sentry.dev/engineering-practices/rust.md#development-environment)

We use VSCode for development. Each repository contains settings files configuring code style, linters, and useful features. When opening the project for the first time, make sure to *"Install the Recommended Extensions"*, as they will allow editor assist during coding. The probably most up-to-date example can always be found in the [Relay repository](https://github.com/getsentry/relay/blob/master/.vscode/extensions.json):

* *Rust Analyzer:* A fast and feature-packed language server integration.
* *CodeLLDB:* Debugger based on LLDB, required to debug with the Rust Analyzer extension
* *Better TOML and Crates:* Support for `Cargo.toml` files and bumping dependency versions

Recommended Rust Analyzer settings in your project's `.vscode/settings.json`:

```jsx
{
  // Enable all features
  "rust-analyzer.cargo.features": "all",

  // If you don't like inline type annotations, this disables it unless Ctrl + Alt is pressed.
  "editor.inlayHints.enabled": "offUnlessPressed",

  // Import rules
  "rust-analyzer.imports.granularity.group": "module",
  "rust-analyzer.imports.prefix": "crate"
}
```

For testing, we often use the snapshot library `[insta](https://github.com/mitsuhiko/insta)`. By default, when running tests with Cargo, insta will compare snapshots and pretty-print diffs on standard out. However, to get most out of insta, install `cargo-insta` and use the cargo command instead:

```bash
# Install insta. This will upgrade outdated versions.
$ cargo install cargo-insta

# To review snapshot diffs interactively:
$ cargo insta review --all

# To run all tests skipping failures:
$ cargo insta test --review

# To quickly reject all pending diffs:
$ cargo insta reject --all
```

## [Makefiles](https://develop.sentry.dev/engineering-practices/rust.md#makefiles)

We use `Makefiles` to collect the most standard actions. A full Makefile for a workspace comprising multiple crates should roughly look like this (can be simplified):

```Makefile
all: check test
.PHONY: all

check: style lint
.PHONY: check

test: test-default test-all
.PHONY: test

test-default:
	cargo test --all
.PHONY: test-default

test-all:
	cargo test --all --all-features
.PHONY: test-all

style:
	@rustup component add rustfmt --toolchain stable 2> /dev/null
	cargo +stable fmt --all -- --check
.PHONY: style

lint:
	@rustup component add clippy --toolchain stable 2> /dev/null
	cargo +stable clippy --all-features --all --tests --examples -- -D clippy::all
.PHONY: lint

format:
	@rustup component add rustfmt --toolchain stable 2> /dev/null
	cargo +stable fmt --all
.PHONY: format

doc:
	cargo doc --workspace --all-features --no-deps
.PHONY: doc
```

## [Cargo Version Numbers](https://develop.sentry.dev/engineering-practices/rust.md#cargo-version-numbers)

Cargo follows [Semantic Versioning](https://semver.org/), which uses X.Y.Z which are respectively called *major*, *minor* and *patch* version numbers. There are two major cases:

### [After 1.0](https://develop.sentry.dev/engineering-practices/rust.md#after-10)

Once a 1.0 release is reached following SemVer is easy:

* Breaking changes require bumping the major version.
* New features bump the minor version.
* Bugfix-only releases bump the patch version.

### [Before 1.0](https://develop.sentry.dev/engineering-practices/rust.md#before-10)

Before a 1.0 release is reached [anything goes according to the spec](https://semver.org/#spec-item-4). However Cargo has [strictly defined update rules for caret requirements](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html) (caret requirements are the default if plain version numbers are used). So in order to make `cargo update` behave well we adopt the following:

* Breaking changes require bumping the minor (Y) version.
* New features bump the patch (Z) version.
* Bugfix-only releases bump the patch (Z) version.

## [Further Reading](https://develop.sentry.dev/engineering-practices/rust.md#further-reading)

* [The Little Book of Rust Macros](https://danielkeep.github.io/tlborm/book/index.html)
* [Rust Nomicon: The Dark Arts of Unsafe Rust](https://doc.rust-lang.org/nomicon/)
* [Rust by Example: A comprehensive list of examples](https://doc.rust-lang.org/stable/rust-by-example/)
* [Rust for Rustaceans: Idiomatic Programming for Experienced Developers](https://rust-for-rustaceans.com/)
* [Rust in Action: Systems programming concepts and techniques](https://www.rustinaction.com/)
* [Zero To Production In Rust: An introduction to backend development](https://www.zero2prod.com/)
* [Rust Atomics and Locks](https://marabos.nl/atomics/)
