Skip to content

Rust is a modern programming language designed for performance and safety. We’ll cover here the basics when coming from a TypeScript background. Some things are very similar, some others are worth longer explanations.

  • Variables can be freely redefined. You can have multiple statements in a row doing let foo = ...;:
let foo = 1;
let foo = String::from("bar");
  • Their content, however, is immutable by default. The declaration must be annotated with mut to allow mutating the object stored in the variable:
let mut items = Vec::new();
items.push("foo");
  • Rust uses iterators a lot. It’s a powerful tool for processing collections of data efficiently, and thanks to the trait system, you can add new methods to existing iterators:
items.iter()
.filter(|item| !item.starts_with("_"))
.map(|item| item.to_uppercase())
.collect::<Vec<_>>();

Rather than throwing exceptions, Rust uses a Result type to handle errors. This generic type accepts two parameters: T represents the type of the successful result, and E represents the type of the error.

The trick is a special operator, ?. This operator, which can only be used in functions that return a Result, allows you to propagate errors up the call stack.

In the example below, if input.parse() returns an Err(...), parse_number will immediately stop its execution and return that err:

fn parse_number(input: &str) -> Result<i32, ParseIntError> {
let number = input.parse()?;
Ok(number)
}

Rust uses a similar syntax to JavaScript’s async/await:

async fn my_task() {
do_something().await;
}

Instead of working with promises, Rust uses Futures. Both are very similar, but futures only get executed when awaited. Another difference is that futures’ typing encode not only the result of the computation, but also whoever generated it.

In the following example, even if both futures return the same type, because they are generated by different functions, they are not the same type:

async fn do_something_1() -> () {}
async fn do_something_2() -> () {}
fn main() {
let mut items = vec![];
// error[E0308]: mismatched types
items.push(do_something_1());
items.push(do_something_2());
}

They must be “boxed” by using a special type, BoxFuture:

use futures::future::BoxFuture;
async fn do_something_1() -> () {}
async fn do_something_2() -> () {}
fn main() {
let mut items = vec![];
items.push(BoxFuture::new(do_something_1()));
items.push(BoxFuture::new(do_something_2()));
}

However the borrow checker is very careful with lifetimes; passing references to async functions can be tricky when the compiler cannot prove that the references will be valid for the duration of the async function execution:

async fn do_something(val: &i32) {
// ...
}
async fn multiple_things_in_parallel() {
let mut futures = Vec::new();
for i in 0..10 {
// error[E0597]: `i` does not live long enough
futures.push(do_something(&i));
}
}

In the example above we pass a reference to the memory where i is stored; the compiler is smart enough to understand that this memory space will be rewritten after each for iteration, so it refuses to pass it to do_something (note that we’re not calling await, so unlike JavaScript’s promises do_something doesn’t immediately start inside the loop).

To wait for all futures to complete, you can use the join_all function, similar to Promise.allSettled in JavaScript:

async fn multiple_things_in_parallel() {
// ...
futures::future::join_all(futures).await;
}

This works well enough when you know in advance how many futures you will be waiting for. That’s not always the case, especially in a package manager where tasks may cascade into other tasks. A solution to this problem is to use a FuturesUnordered container, which allows pushing new futures.