The 9 indispensable features to learn for the new Rust programmer

Rust is a rather large and complex programming language with a lot of features. But I have good news: less than 20% of the features will bring you more than 80% of the results.

Here are the features I consider indispensable to learn when you are starting Rust.

Ready to dive? 🦀


Enums (also called algebraic data types) are certainly the favorite feature of new Rustaceans because they are the foundations of Result and Option.

enum Result<T, E> {

pub enum Option<T> {

Enums allow developers to safely encode into code all the possible states of their programs and check at compile time that they didn't forget a case:

#[derive(Debug, Clone, Copy)]
enum Platform {

impl fmt::Display for Platform {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Platform::Linux => write!(f, "Linux"),
            Platform::Macos => write!(f, "macOS"),
            // Compile time error! We forgot Windows and Unknown


Threads were designed to parallelize compute-intensive tasks. However, these days, a lot of applications (such as a network scanner or a web server) are I/O (Input / Output) intensive which means that by using threads, our apps would spend a lot of time waiting for network requests to complete and use way more resources than necessary.

These are the problems solved by async-await, all while providing a great developer experience.

You can learn more about how async-await works in my previous posts: Async Rust: Cooperative vs Preemptive scheduling and Async Rust: What is a runtime? Here is how tokio works under the hood.


You may need to switch concrete implementations of multiple similar types sharing the same behavior.

For example, a storage driver:

struct FilesystemStorage {
  get() // ...
  put() // ...
  delete() // ...

struct S3Storage {
  get() // ...
  put() // ...
  delete() // ...

For that, we use traits, also called interfaces in other languages.

trait Storage {
  get() // ...
  put() // ...
  delete() // ...

impl Storage for FilesystemStorage {
  // ...

impl Storage for S3Storage {
  // ...

fn use_storage<S: Storage>(storage: S) {
  // ...

Smart pointers

I've already extensively covered smart pointer on this blog. In short, they allow developers to avoid lifetime annotations and thus write cleaner code.

They also are the foundations of traits obejcts which allow you to pick the right implementation at runtime (instead of compile-time with generics).

struct MyService {
  db: Arc<DB>,
  mailer: Arc<dyn drivers::Mailer>,
  storage: Arc<dyn drivers::Storage>,
  other_service: Arc<other::Service>,


Rust's standard library's collections are what make writing complex algorithms and business logic in Rust is so pleasant.

let dedup_subdomains: HashSet<String> = subdomains.into_iter().collect();


An Iterator is an object that enables developers to traverse collections. They can be obtained from most of the collections of the standard library.

fn filter() {
    let v = vec![-1, 2, -3, 4, 5].into_iter();

    let _positive_numbers: Vec<i32> = v.filter(|x: &i32| x.is_positive()).collect();

Iterators are lazy: they won't do anything if they are not consumed.


Combinators are a very interesting topic. Almost all the definitions you'll find on the internet will make your head explode 🤯 because they raise more questions than they answer.

Thus, here is my empiric definition: Combinators are methods that ease the manipulation of some type T. They favor a functional (method chaining) style of code.

let sum: u64 = vec![1, 2, 3].into_iter().map(|x| x * x).sum();

Here are a more examples:

// Convert a `Result` to an `Option`
fn result_ok() {
    let _port: Option<String> = std::env::var("PORT").ok();

// Use a default `Result` if `Result` is `Err`
fn result_or() {
    let _port: Result<String, std::env::VarError> =

// Use a default value if empty, then apply a function
let http_port = std::env::var("PORT")
    .map_or(Ok(String::from("8080")), |env_val| env_val.parse::<u16>())?;

// Chain a function if `Result` is `Ok` or a different function if `Result` is `Err`
let master_key = std::env::var("MASTER_KEY")
    .map_err(|_| env_not_found("MASTER_KEY"))


Streams can be roughly defined as iterators for the async world.

You should use them when you want to apply asynchronous operations on a sequence of items of the same type, whether it be a network socket, a file, or a long-lived HTTP request.

Anything that is too large to fit in memory and thus should be split in smaller chunks, or that may arrive later, but we don't know when, or that is simply a collection (a Vec or an HashMap for example) to which we need to apply async operations to.

They also allow us to easily execute operations concurrently:

async fn compute_job(job: i64) -> i64 {
  // ...

async fn main() {
    let jobs = 0..100;
    let concurrency = 42;

        .for_each_concurrent(concurrency, |job| compute_job(job)).await;

You can learn more about using streams as worker pools in my previous post: How to implement worker pools in Rust


Finally, Rust is very well suited for embedded development and shellcodes. Because these environments don't rely on a proper Operating System, you generally can't use Rust's standard library and you need to use the core library instead.

For these usecases, we use the #![no_std] attribute:


fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}

fn _start() {
  // ...
1 email / week to learn how to (ab)use technology for fun & profit: Programming, Hacking & Entrepreneurship.
I hate spam even more than you do. I'll never share your email, and you can unsubscribe at any time.

Tags: programming, rust, tutorial

Want to learn Rust, Cryptography and Security? Get my book Black Hat Rust!