Introduction

This is a (non-comprehensive) guide for JavaScript developers that are completely new to the Rust programming language. Some concepts and constructs translate fairly well between JavaScript and Rust, but which may be expressed differently, whereas others are a radical departure, like memory management. This guide provides a brief comparison and mapping of those constructs and concepts with concise examples.

The original authors1 of this guide were themselves JavaScript developers who were completely new to Rust. It is the guide the authors wish they had when they started on their Rust journey. That said, the authors would encourage you to read books and other material available on the Web to embrace Rust and its idioms rather than attempting to learn it exclusively through the lens of JavaScript. Meanwhile, this guide can help answers some question quickly, like: Does Rust support inheritance, threading, asynchronous programming, etc.? Assumptions:

  • Reader is a seasoned JavaScript developer.
  • Reader is completely new to Rust.

Goals:

  • Provide a brief comparison and mapping of various JavaScript topics to their counterparts in Rust.
  • Provide links to Rust reference, book and articles for further reading on topics.

Non-goals:

  • Discussion of design patterns and architectures.
  • Tutorial on the Rust language.
  • Reader is proficient in Rust after reading this guide.
  • While there are short examples that contrast JavaScript and Rust code for some topics, this guide is not meant to be a cookbook of coding recipes in the two languages.

1

The original authors of Microsoft's Rust for C#/.NET Developers were (in alphabetical order): Atif Aziz, Bastian Burger, Daniele Antonio Maggio, Dariusz Parys and Patrick Schuler.

The adaption work is done by @DevScholar on GitHub.

This book contains artificial intelligence generated code, and the code is audited.

License

MIT License

Copyright (c) Contributors.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

This book is adapted from Microsoft's Rust for C#/.NET Developers.

MIT License

Copyright (c) Microsoft Corporation. Portions Copyright (c) 2010 The Rust Project Developers

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Contributing

You are invited to contribute 💖 to this guide by opening issues and submitting pull requests!

Here are some ideas 💡 for how and where you can help most with contributions:

  • Fix any spelling or grammatical mistakes you see as you read.

  • Fix technical inaccuracies.

  • Fix logical or compilation errors in code examples.

  • Improve the English, especially if it's your native tongue or you have excellent proficiency in the language.

  • Expand an explanation to provide more context or improve the clarity of some topic or concept.

  • Keep it fresh with changes in JavaScript and Rust. For example, if there is a change in JavaScript or Rust that brings the two languages closer together then some parts, including sample code, may need revision.

If you're making a small to modest correction, such fixing a spelling error or a syntax error in a code example, then feel free to submit a pull request directly. For changes that may require a large effort on your part (and reviewers as a result), it is strongly recommended that you submit an issue and seek approval of the maintainers/editors before investing your time. It will avoid heartbreak 💔 if the pull request is rejected for various reasons.

Making quick contributions has been made super simple. If you see an error on a page and happen to be online, you can click edit icon 📝 in the corner of the page to edit the Markdown source of the content and submit a change.

Contribution Guidelines

  • Stick to the goals of this guide laid out in the introduction; put another way, avoid the non-goals!

  • Prefer to keep text short and use short, concise and realistic code examples to illustrate a point.

  • As much as it is possible, always provide and compare examples in Rust and JavaScript.

  • Feel free to use latest JavaScript/Rust language features if it makes an example simpler, concise and alike across the two languages.

  • Avoid using community packages in JavaScript examples. Stick to the JavaScript Global Objects as much as possible. Since the Rust Standard Library has a much smaller API surface, it is more acceptable to call out crates for some functionality, should it be necessary for illustration (like rand for random number generation), but make sure they are mature, popular and rusted.

  • Make example code as self-contained as possible and runnable (unless the idea is to illustrate a compile-time or run-time error).

  • Maintain the general style of this guide, which is to avoid using you as if the reader is being told or instructed; use the third-person voice instead. For example, instead of saying, “You represent optional data in Rust with the Option<T> type”, write instead, “Rust has the Option<T> type that is used to represent optional data”.

Getting Started

Rust Playground

The easiest way to get started with Rust without needing any local installation is to use the Rust Playground. It is a minimal development front-end that runs in the Web browser and allows writing and running Rust code.

Dev Container

The execution environment of the Rust Playground has some limitations, such as total compilation/execution time, memory and networking so another option that does not require installing Rust would be to use a dev container, such as the one provided in the repository https://github.com/microsoft/vscode-remote-try-rust. Like Rust Playground, the dev container can be run directly in a Web browser using GitHub Codespaces or locally using Visual Studio Code.

Local Install

For a complete local installation of Rust compiler and its development tools, see the Installation section of the Getting Started chapter in the The Rust Programming Language book, or the Install page at rust-lang.org.

Language

This sections compares JavaScript and Rust language features.

Scalar Types

The following table lists the primitive types in Rust and their equivalent in JavaScript:

RustJavaScriptNote
boolboolean
charstringSee note 1.
i8numberSee note 2.
i16number
i32number
i64number/bigint
i128number/bigint
isizeNumber.MAX_SAFE_INTEGER
u8number
u16number
u32number
u64number/bigint
u128number/bigint
usizeNumber.MAX_SAFE_INTEGER
f32number
f64number
number
()null
undefined
objectSee note 3.

Notes:

  1. char in Rust and string in JavaScript have different definitions. In Rust, a char is 4 bytes wide that is a Unicode scalar value, but in JavaScript, a character is 2 bytes wide and stores the character using the UTF-16 encoding. There is no char type equivalent in JavaScript, only string. For more information, see the Rust char documentation.
  2. There are only three number data type in JavaScript, number, which is essentially a floating point number. And the bigint type for storing numbers that exceed the range -(253 - 1) (Number.MIN_SAFE_INTEGER) to 253 - 1 (Number.MAX_SAFE_INTEGER). and the bigdecimal type for storing high-precision decimals.
  3. For historical reasons, JavaScript has two empty data types: null and undefined. undefined denotes a value that was never created, and null denotes a value that was created but intentionally left empty. See also:

Strings

There are two string types in Rust: String and &str. The former is allocated on the heap and the latter is a slice of a String or a &str.

The mapping of those to JavaScript is shown in the following table:

RustJavaScriptNote
&mut strN/A
&strN/A
Box<str>stringsee Note 1.
Stringstring
String (mutable)stringsee Note 1.

There are differences in working with strings in Rust and JavaScript, but the equivalents above should be a good starting point. One of the differences is that Rust strings are UTF-8 encoded, but JavaScript strings are UTF-16 encoded. Rust strings can be mutable when declared as such, for example let s = &mut String::from("hello");.

There are also differences in using strings due to the concept of ownership. To read more about ownership with the String Type, see the Rust Book.

Notes:

  1. JavaScript has only one string type, string. JavaScript has no pointer types.

JavaScript:

let str = "Hello, World!"; 
let str = new String("Hello, World!")

Rust:

let str = Box::new("Hello World!");
let mut sb = String::from("Hello World!");

String Literals

String literals in JavaScript String types and allocated on the heap. In Rust, they are &'static str, which is immutable and has a global lifetime and does not get allocated on the heap; they're embedded in the compiled binary. JavaScript:

let str = "Hello, World!";

Rust:

let str: &'static str = "Hello, World!";

JavaScript verbatim string literals are equivalent to Rust raw string literals.

JavaScript

let str = `Hello, \World/!`;

Rust

let str = r#"Hello, \World/!"#;

Rust UTF-8 string literals:

let str = b"hello";

String Interpolation

JavaScript has a built-in string interpolation feature that allows you to embed expressions inside a string literal. The following example shows how to use string interpolation in JavaScript:

let name = "John";
let age = 42;
let str = `Person Name: ${name}, Age: ${age} `;

Rust does not have a built-in string interpolation feature. Instead, the format! macro is used to format a string. The following example shows how to use string interpolation in Rust:

let name = "John";
let age = 42;
let str = format!("Person {{ name: {name}, age: {age} }}");

Custom classes and structs can also be interpolated in JavaScript due to the fact that the toString() method is available for each type as it inherits from object.

class Person {
    constructor({
        name,
        age
    }) {
        this.name = name;
        this.age = age;
        this.toString = function() {
            return `Person Name: ${name}, Age: ${age}`;
        }
    }

}
let person = new Person({
    name: "John",
    age: 42
});
console.log(person);

In Rust, there is no default formatting implemented/inherited for each type. Instead, the std::fmt::Display trait must be implemented for each type that needs to be converted to a string.

use std::fmt::*;

struct Person {
    name: String,
    age: i32,
}

impl Display for Person {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "Person {{ name: {}, age: {} }}", self.name, self.age)
    }
}

let person = Person {
    name: "John".to_owned(),
    age: 42,
};

println!("{person}");

Another option is to use the std::fmt::Debug trait. The Debug trait is implemented for all standard types and can be used to print the internal representation of a type. The following example shows how to use the derive attribute to print the internal representation of a custom struct using the Debug macro. This declaration is used to automatically implement the Debug trait for the Person struct:

#[derive(Debug)]
struct Person {
    name: String,
    age: i32,
}

let person = Person {
    name: "John".to_owned(),
    age: 42,
};

println!("{person:?}");

Note: Using the :? format specifier will use the Debug trait to print the struct, where leaving it out will use the Display trait.

See also:

Structured Types

Commonly used object and collection types in JavaScript and their mapping to Rust.

JavaScriptRust
arrayArray
arrayVec
arrayTuple
objectHashMap

Array

Fixed arrays are supported the same way in Rust as in JavaScript.

JavaScript:

let someArray = [1,2];

Rust:

let someArray: [i32; 2] = [1,2];

List

In Rust the equivalent of a array is a Vec<T>. Arrays can be converted to Vecs and vice versa.

JavaScript:

let something = ["a", "b"];
something.push("c");

Rust:

let mut something = vec![
    "a".to_owned(),
    "b".to_owned()
];

something.push("c".to_owned());

Tuples

JavaScript:

const let something = [1, 2];
console.log(`a = ${something[0]} b = ${something[1]}`);

Rust:

let something = (1, 2);
println!("a = {} b = {}", something.0, something.1);

// deconstruction supported
let (a, b) = something;
println!("a = {} b = {}", a, b);

NOTE: Rust tuple elements cannot be named. The only way to access a tuple element is by using the index of the element or deconstructing the tuple.

Dictionary

In Rust the equivalent of a Dictionary<TKey, TValue> is a object.

JavaScript:

var something = {
    "Foo": "Bar",
    "Baz": "Qux"
};

something["hi"] = "there";

Rust:

let mut something = HashMap::from([
    ("Foo".to_owned(), "Bar".to_owned()),
    ("Baz".to_owned(), "Qux".to_owned())
]);

something.insert("hi".to_owned(), "there".to_owned());

See also:

Custom Types

The following sections discuss various topics and constructs related to developing custom types:

Classes

Rust doesn't have classes. It only has structures or struct.

Records

Rust doesn't have any construct for authoring records.

Structures (struct)

In JavaScript, there is no direct concept of a structure, but you can use objects to model similar structures. Structures in Rust:

  • In Rust, struct simply defines the data/fields. The behavioural aspects in terms of functions and methods, are defined separately in an implementation block (impl).

  • They can implement multiple traits in Rust.

  • They cannot be sub-classed.

  • They are allocated on stack by default, unless:

    • In Rust, wrapped in a smart pointer like Box, Rc/Arc.

In Rust, a struct is the primary construct for modeling any data structure (the other being an enum).

A struct in Rust requires just one more step using the #derive attribute and listing the traits to be implemented:

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32,
}

Value types in JavaScript are usually designed by a developer to be mutable. It's considered best practice speaking semantically, but the language does not prevent designing a struct that makes destructive or in-place modifications. In Rust, it's the same. A type has to be consciously developed to be immutable.

Since Rust doesn't have classes and consequently type hierarchies based on sub-classing, shared behaviour is achieved via traits and generics and polymorphism via virtual dispatch using trait objects.

In JavaScript:

class Rectangle {
    constructor(x1, y1, x2, y2) {
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
    }

    length() {
        return this.y2 - this.y1;
    }

    width() {
        return this.x2 - this.x1;
    }

    top_left() {
        return [this.x1, this.y1];
    }

    bottom_right() {
        return [this.x2, this.y2];
    }

    area() {
        return this.length() * this.width();
    }

    is_square() {
        return this.width() === this.length();
    }

    toString() {
        return `(${this.x1}, ${this.y1}), (${this.x2}, ${this.y2})`;
    }
}

const rect = new Rectangle(0, 0, 4, 4);
console.log(rect.area());
console.log(rect.toString());

The equivalent in Rust would be:

#![allow(dead_code)]

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }

    pub fn x1(&self) -> i32 { self.x1 }
    pub fn y1(&self) -> i32 { self.y1 }
    pub fn x2(&self) -> i32 { self.x2 }
    pub fn y2(&self) -> i32 { self.y2 }

    pub fn length(&self) -> i32 {
        self.y2 - self.y1
    }

    pub fn width(&self)  -> i32 {
        self.x2 - self.x1
    }

    pub fn top_left(&self) -> (i32, i32) {
        (self.x1, self.y1)
    }

    pub fn bottom_right(&self) -> (i32, i32) {
        (self.x2, self.y2)
    }

    pub fn area(&self)  -> i32 {
        self.length() * self.width()
    }

    pub fn is_square(&self)  -> bool {
        self.width() == self.length()
    }
}

use std::fmt::*;

impl Display for Rectangle {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "({}, {}), ({}, {})", self.x1, self.y2, self.x2, self.y2)
    }
}

Since there is no inheritance in Rust, the way a type advertises support for some formatted representation is by implementing the Display trait. This then enables for an instance of the structure to participate in formatting, such as shown in the call to println! below:

fn main() {
    let rect = Rectangle::new(12, 34, 56, 78);
    println!("Rectangle = {rect}");
}

Interfaces

Rust doesn't have interfaces. It has traits, instead. Similar to an interface, a trait represents an abstraction and its members form a contract that must be fulfilled when implemented on a type.

In Rust, trait implementing the interface/trait can subsequently provide a more suitable and/or optimized implementation.

Traits in Rust can have (instance-based) method, associated functions and constants.

Apart from class hierarchies, interfaces are a core means of achieving polymorphism via dynamic dispatch for cross-cutting abstractions. They enable general-purpose code to be written against the abstractions represented by the interfaces without much regard to the concrete types implementing them. The same can be achieved with Rust's trait objects in a limited fashion. A trait object is essentially a v-table (virtual table) identified with the dyn keyword followed by the trait name, as in dyn Shape (where Shape is the trait name). Trait objects always live behind a pointer, either a reference (e.g. &dyn Shape) or the heap-allocated Box (e.g. Box<dyn Shape>). The passing limitation of trait objects mentioned earlier, is that the original implementing type cannot be recovered. In other words, whereas it's quite common to downcast or test an interface to be an instance of some other interface or sub- or concrete type, the same is not possible in Rust (without additional effort and support).

Enumeration types (enum)

To convert a Rust enum to JavaScript, you can represent it using an object with key-value pairs.

const DayOfWeek = {
    Sunday: 0,
    Monday: 1,
    Tuesday: 2,
    Wednesday: 3,
    Thursday: 4,
    Friday: 5,
    Saturday: 6
};

Rust syntax for doing the same:

enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
}

An instance of an enum type in Rust does not have any pre-defined behaviour that's inherited. It cannot even participate in equality checks as simple as dow == DayOfWeek::Friday.

#[derive(Debug,     // enables formatting in "{:?}"
         Clone,     // required by Copy
         Copy,      // enables copy-by-value semantics
         Hash,      // enables hash-ability for use in map types
         PartialEq  // enables value equality (==)
)]
enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
}

fn main() {
    let dow = DayOfWeek::Wednesday;
    println!("Day of week = {dow:?}");

    if dow == DayOfWeek::Friday {
        println!("Yay! It's the weekend!");
    }

    // coerce to integer
    let dow = dow as i32;
    println!("Day of week = {dow:?}");

    let dow = dow as DayOfWeek;
    println!("Day of week = {dow:?}");
}

As the example above shows, an enum can be coerced to its assigned integral value. It's up to the developer to provide such a helper function:

impl DayOfWeek {
    fn try_from_i32(n: i32) -> Result<DayOfWeek, i32> {
        use DayOfWeek::*;
        match n {
            0 => Ok(Sunday),
            1 => Ok(Monday),
            2 => Ok(Tuesday),
            3 => Ok(Wednesday),
            4 => Ok(Thursday),
            5 => Ok(Friday),
            6 => Ok(Saturday),
            _ => Err(n)
        }
    }
}

The try_from_i32 function returns a DayOfWeek in a Result indicating success (Ok) if n is valid. Otherwise it returns n as-is in a Result indicating failure (Err):

let dow = DayOfWeek::try_from_i32(5);
println!("{dow:?}"); // prints: Ok(Friday)

let dow = DayOfWeek::try_from_i32(50);
println!("{dow:?}"); // prints: Err(50)

There exist crates in Rust that can help with implementing such mapping from integral types instead of having to code them manually.

An enum type in Rust can also serve as a way to design (discriminated) union types, which allow different variants to hold data specific to each variant. For example:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

This form of enum declaration does not exist in JavaScript, but it can be emulated with classes:

class IpAddr {
    constructor(v4, v6) {
        this.v4 = v4;
        this.v6 = v6;
    }
}

const home = new IpAddr([127, 0, 0, 1], null);
const loopback = new IpAddr(null, "::1");

The difference between the two is that the Rust definition produces a closed type over the variants. In other words, the compiler knows that there will be no other variants of IpAddr except IpAddr::V4 and IpAddr::V6, and it can use that knowledge to make stricter checks.

Members

Constructors

Rust does not have any notion of constructors. Instead, you just write factory functions that return an instance of the type. The factory functions can be stand-alone or associated functions of the type. Conventionally, if there is just one factory function for a struct, it's named new:

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }
}

Since Rust functions (associated or otherwise) do not support overloading; the factory functions have to be named uniquely. For example, below are some examples of so-called constructors or factory functions available on String:

  • String::new: creates an empty string.
  • String::with_capacity: creates a string with an initial buffer capacity.
  • String::from_utf8: creates a string from bytes of UTF-8 encoded text.
  • String::from_utf16: creates a string from bytes of UTF-16 encoded text.

In the case of an enum type in Rust, the variants act as the constructors. See the section on enumeration types for more.

See also:

Methods (static & instance-based)

Rust types (both enum and struct), can have static and instance-based methods. In Rust-speak, a method is always instance-based and is identified by the fact that its first parameter is named self. The self parameter has no type annotation since it's always the type to which the method belongs. A static method is called an associated function. In the example below, new is an associated function and the rest (length, width and area) are methods of the type:

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }

    pub fn length(&self) -> i32 {
        self.y2 - self.y1
    }

    pub fn width(&self)  -> i32 {
        self.x2 - self.x1
    }

    pub fn area(&self)  -> i32 {
        self.length() * self.width()
    }
}

Constants

A type in Rust can have constants. However, the most interesting aspect to note is that Rust allows a type instance to be defined as a constant too:

struct Point {
    x: i32,
    y: i32,
}

impl Point {
    const ZERO: Point = Point { x: 0, y: 0 };
}

In JavaScript, you need to define a Point class, and then, simulate the construction behavior in Rust by setting a static property ZERO directly on the Point class.

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}

Point.ZERO = new Point(0, 0);

Events

Rust has no built-in support for type members to adverstise and fire events.

Properties

Rust only has methods where a getter is named after the field (in Rust method names can share the same identifier as a field) and the setter uses a set_ prefix.

Below is an example showing how property-like accessor methods typically look for a type in Rust:

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }

    // like property getters (each shares the same name as the field)

    pub fn x1(&self) -> i32 { self.x1 }
    pub fn y1(&self) -> i32 { self.y1 }
    pub fn x2(&self) -> i32 { self.x2 }
    pub fn y2(&self) -> i32 { self.y2 }

    // like property setters

    pub fn set_x1(&mut self, val: i32) { self.x1 = val }
    pub fn set_y1(&mut self, val: i32) { self.y1 = val }
    pub fn set_x2(&mut self, val: i32) { self.x2 = val }
    pub fn set_y2(&mut self, val: i32) { self.y2 = val }

    // like computed properties

    pub fn length(&self) -> i32 {
        self.y2 - self.y1
    }

    pub fn width(&self)  -> i32 {
        self.x2 - self.x1
    }

    pub fn area(&self)  -> i32 {
        self.length() * self.width()
    }
}

Extension Methods

In JavaScript, you can use prototype to add new methods to existing classes. This approach allows you to add new behavior to an existing class without changing the existing class definition:

//JavaScript doesn't have a StringBuilder class. This code is only used to demonstrate adding a new method to an existing class.
class StringBuilder {
    constructor(initialString) {
        this.value = initialString;
    }

    toString() {
        return this.value;
    }
}
StringBuilder.prototype.wrap = function (left, right) {
        this.value = left + this.value + right;
    }
const sb = new StringBuilder("Hello, World!");
sb.wrap(">>> ", " <<<");
console.log(sb.toString()); 

Note that for an extension method to become available (2), the namespace with the type containing the extension method must be imported (1). Rust offers a very similar facility via traits, called extension traits. The following example in Rust is the equivalent of the C# example above; it extends String with the method wrap:

#![allow(dead_code)]

mod exts {
    pub trait StrWrapExt {
        fn wrap(&mut self, left: &str, right: &str);
    }

    impl StrWrapExt for String {
        fn wrap(&mut self, left: &str, right: &str) {
            self.insert_str(0, left);
            self.push_str(right);
        }
    }
}

fn main() {
    use exts::StrWrapExt as _; // (1)

    let mut s = String::from("Hello, World!");
    s.wrap(">>> ", " <<<"); // (2)
    println!("{s}"); // Prints: >>> Hello, World! <<<
}
Also note, the extension trait identifier `StrWrapExt` can itself be discarded via `_` at the time of import without affecting the availability of `wrap` for `String`.

Visibility/Access modifiers

In JavaScript, there is no explicit visibility modifier like in C#, but similar functionality can be achieved with some conventions.

In Rust, a compilation is built-up of a tree of modules where modules contain and define items like types, traits, enums, constants and functions. Almost everything is private by default. One exception is, for example, associated items in a public trait, which are public by default. This is similar to how members of a C# interface declared without any public modifiers in the source code are public by default. Rust only has the pub modifier to change the visibility with respect to the module tree. There are variations of pub that change the scope of the public visibility:

  • pub(self)
  • pub(super)
  • pub(crate)
  • pub(in PATH)

For more details, see the Visibility and Privacy section of The Rust Reference.

Mutability

When designing a type in JavaScript, it is not the responsiblity of the developer to decide whether the a type is mutable or immutable; whether it supports destructive or non-destructive mutations. In Rust, mutability is expressed on methods through the type of the self parameter as shown in the example below:

struct Point { x: i32, y: i32 }

impl Point {
    pub fn new(x: i32, y: i32) -> Self {
        Self { x, y }
    }

    // self is not mutable

    pub fn x(&self) -> i32 { self.x }
    pub fn y(&self) -> i32 { self.y }

    // self is mutable

    pub fn set_x(&mut self, val: i32) { self.x = val }
    pub fn set_y(&mut self, val: i32) { self.y = val }
}

In JavaScript, use ES6's destructuring assignment and object extension syntax to implement non-destructive mutation:

class Point {
    constructor(X, Y) {
        this.X = X;
        this.Y = Y;
    }
}

let pt = new Point(123, 456);
pt = { ...pt, X: 789 };
console.log(pt); // prints: Point { X = 789, Y = 456 }

There is no with in Rust, but to emulate something similar in Rust, it has to be baked into the type's design:

struct Point { x: i32, y: i32 }

impl Point {
    pub fn new(x: i32, y: i32) -> Self {
        Self { x, y }
    }

    pub fn x(&self) -> i32 { self.x }
    pub fn y(&self) -> i32 { self.y }

    // following methods consume self and return a new instance

    pub fn set_x(self, val: i32) -> Self { Self::new(val, self.y) }
    pub fn set_y(self, val: i32) -> Self { Self::new(self.x, val) }
}

In JavaScript, classes are used to simulate structs, and destructuring objects is assigned to implement something like with.

class Point {
    constructor(x, y) {
        this.X = x;
        this.Y = y;
    }

    toString() {
        return `(${this.X}, ${this.Y})`;
    }
}

let pt = new Point(123, 456);
console.log(pt.toString()); // prints: (123, 456)
pt = { ...pt, X: 789 };
console.log(pt.toString()); // prints: (789, 456)

Rust has a struct update syntax that may seem similar:

mod points {
    #[derive(Debug)]
    pub struct Point { pub x: i32, pub y: i32 }
}

fn main() {
    use points::Point;
    let pt = Point { x: 123, y: 456 };
    println!("{pt:?}"); // prints: Point { x: 123, y: 456 }
    let pt = Point { x: 789, ..pt };
    println!("{pt:?}"); // prints: Point { x: 789, y: 456 }
}
Since the syntax requires access to the type's fields, it is generally more common to use it within the Rust module that has access to private details of its types.

Local Functions

Rust offer local functions,but local functions in Rust cannot use variables from their surrounding lexical scope;but closures can.

Lambda and Closures

Rust allows functions to be used as first-class values that enable writing higher-order functions. Higher-order functions are essentially functions that accept other functions as arguments to allow for the caller to participate in the code of the called function.

Rust has function pointers with the fn type being the simplest:

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(|x| x + 1, 5);
    println!("The answer is: {}", answer); // Prints: The answer is: 12
}

In JavaScript:

function doTwice(f, arg) {
    return f(arg) + f(arg);
}

function main() {
    const answer = doTwice(x => x + 1, 5);
    console.log(`The answer is: ${answer}`); // Prints: The answer is: 12
}

main();

However, Rust makes a distinction between function pointers (where fn defines a type) and closures: a closure can reference variables from its surrounding lexical scope, but not a function pointer.

Functions and methods that accept closures are written with generic types that are bound to one of the traits representing functions: Fn, FnMut and FnOnce. When it's time to provide a value for a function pointer or a closure, a Rust developer uses a closure expression (like |x| x + 1 in the example above). Whether the closure expression creates a function pointer or a closure depends on whether the closure expression references its context or not.

When a closure captures variables from its environment then ownership rules come into play because the ownership ends up with the closure. For more information, see the “Moving Captured Values Out of Closures and the Fn Traits” section of The Rust Programming Language.

Variables

⚠️Be Sure to learn about the ownership in Rust before reading this article.

Consider the following example around variable assignment in JavaScript:

let x = 5;

And the same in Rust:

let x: i32 = 5;

Rust is type-safe: the compiler guarantees that the value stored in a variable is always of the designated type. The example can be simplified by using the compiler's ability to automatically infer the types of the variable. In JavaScript:

let x = 5;

In Rust:

let x = 5;

When expanding the first example to update the value of the variable (reassignment), the behavior of JavaScript and Rust differ:

let x = 5;
x = 6;
console.log(x); // 6

In Rust, the identical statement will not compile:

let x = 5;
x = 6; // Error: cannot assign twice to immutable variable 'x'.
println!("{}", x);

In Rust, variables are immutable by default. Once a value is bound to a name, the variable's value cannot be changed. Variables can be made mutable by adding mut in front of the variable name:

let mut x = 5;
x = 6;
println!("{}", x); // 6

Rust offers an alternative to fix the example above that does not require mutability through variable shadowing:

let x = 5;
let x = 6;
println!("{}", x); // 6

JavaScript also supports shadowing, e.g. locals can shadow fields and type members can shadow members from the base type. In Rust, the above example demonstrates that shadowing also allows to change the type of a variable without changing the name, which is useful if one wants to transform the data into different types and shapes without having to come up with a distinct name each time.

See also:

Namespaces

In JavaScript, there is no such thing as a namespace. Instead, JavaScript developers use sub-objects or conventions to implement namespace-like functionality.

In Rust, namespace refers to a different concept. The equivalent of a namespace in Rust is a module. For Rust, visibility of items can be restricted using access modifiers, respectively visibility modifiers. In Rust, the default visibility is private (with only few exceptions). For more fine-grained access control, refer to the visibility modifiers reference.

Equality

When comparing for equality in JavaScript, this refers to testing for equivalence insome cases (also known as value equality), and in other cases it refers to testing for reference equality, which tests whether two variables refer to the same underlying object in memory. In JavaScript, while there is no syntax for explicitly custom types, custom types can be simulated through constructors and prototypes. Constructors allow you to create objects with specific properties and methods, and you can use prototypes to implement inheritance and shared methods. Every "custom type" can be compared for equality because it inherits from object.

For example, when comparing for equivalence and reference equality in JavaScript:

class Point {
    constructor(X, Y) {
        this.X = X;
        this.Y = Y;
    }
    
    equals(other) {
        return this.X === other.X && this.Y === other.Y;
    }
}

const a = new Point(1, 2);
const b = new Point(1, 2);
const c = a;

console.log(a.equals(b)); // (1) true
console.log(a.equals(new Point(2, 2))); // (1) false
console.log(a === b); // (2) false
console.log(a === c); // (2) true
  1. In JavaScript, classes are used to implement equals methods to compare the equality of values.
  2. For the comparison of reference equality, using the === operator to check if the variable points to the same object in memory.

Equivalently in Rust:

#[derive(Copy, Clone)]
struct Point(i32, i32);

fn main() {
    let a = Point(1, 2);
    let b = Point(1, 2);
    let c = a;
    println!("{}", a == b); // Error: "an implementation of `PartialEq<_>` might be missing for `Point`"
    println!("{}", a.eq(&b));
    println!("{}", a.eq(&Point(2, 2)));
}

The compiler error above illustrates that in Rust equality comparisons are always related to a trait implementation. To support a comparison using ==, a type must implement PartialEq.

Fixing the example above means deriving PartialEq for Point. Per default, deriving PartialEq will compare all fields for equality, which therefore have to implement PartialEq themselves.

#[derive(Copy, Clone, PartialEq)]
struct Point(i32, i32);

fn main() {
    let a = Point(1, 2);
    let b = Point(1, 2);
    let c = a;
    println!("{}", a == b); // true
    println!("{}", a.eq(&b)); // true
    println!("{}", a.eq(&Point(2, 2))); // false
    println!("{}", a.eq(&c)); // true
}

See also:

  • Eq for a stricter version of PartialEq.

Generics

Generics provide a way to create definitions for types and methods that can be parameterized over other types. This improves code reuse, type-safety and performance (e.g. avoid run-time casts). Consider the following example of a generic type that adds a timestamp to any value. However, JavaScript does not have the concept of generics.

class Timestamped {
    constructor(value) {
        this.Timestamp = new Date();
        this.Value = value;
    }
}

Rust has generics as shown by the equivalent of the above:

use std::time::*;

struct Timestamped<T> { value: T, timestamp: SystemTime }

impl<T> Timestamped<T> {
    fn new(value: T) -> Self {
        Self { value, timestamp: SystemTime::now() }
    }
}

See also:

Generic type constraints

JavaScript has no concept of generics, and it is a weakly typed scripting language that makes it impossible to add type constraints to it.

class Timestamped {
    constructor(value) {
        this.value = value;
        this.timestamp = Date.now();
    }

    equals(other) {
        return this.value === other.value && this.timestamp === other.timestamp;
    }
}

The same can be achieved in Rust:

use std::time::*;

struct Timestamped<T> { value: T, timestamp: SystemTime }

impl<T> Timestamped<T> {
    fn new(value: T) -> Self {
        Self { value, timestamp: SystemTime::now() }
    }
}

impl<T> PartialEq for Timestamped<T>
    where T: PartialEq {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value && self.timestamp == other.timestamp
    }
}

Generic type constraints are called bounds in Rust.

In Rust, this is flexible because it Timestamped<T> conditionally implements PartialEq. This means that Timestamped<T> instances can still be created for some non-equatable T, but then Timestamped<T> will not implement equality via PartialEq for such a T.

See also:

Polymorphism

Rust does not support classes and sub-classing therefore polymorphism can't be achieved in an identical manner to JavaScript.

See also:

Inheritance

As explained in structures section, Rust does not provide (class-based) inheritance. A way to provide shared behavior between structs is via making use of traits. However, Rust allows to define relationships between traits by using supertraits.

Exception Handling

In JavaScript, an exception should always be an Error object or an instance of an Error subclass. Exceptions are thrown if a problem occurs in a code section. A thrown exception is passed up the stack until the application handles it or the program terminates.

Rust does not have exceptions, but distinguishes between recoverable and unrecoverable errors instead. A recoverable error represents a problem that should be reported, but for which the program continues. Results of operations that can fail with recoverable errors are of type Result<T, E>, where E is the type of the error variant. The panic! macro stops execution when the program encounters an unrecoverable error. An unrecoverable error is always a symptom of a bug.

Custom error types

An example on how to create user-defined exceptions:

class EmployeeListNotFoundException extends Error {
    constructor(message) {
        super(message);
        this.name = 'EmployeeListNotFoundException';
    }
}

In Rust, one can implement the basic expectations for error values by implementing the Error trait. The minimal user-defined error implementation in Rust is:

#[derive(Debug)]
pub struct EmployeeListNotFound;

impl std::fmt::Display for EmployeeListNotFound {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Could not find employee list.")
    }
}

impl std::error::Error for EmployeeListNotFound {}

The equivalent to the JavaScript Error.cause property is the Error::source() method in Rust. However, it is not required to provide an implementation for Error::source(), the blanket (default) implementation returns a None.

Raising exceptions

To raise an error in JavaScript, throw an error:

function throwIfNegative(value) {
    if (value < 0) {
        throw new Error('Value cannot be negative');
    }
}

For recoverable errors in Rust, return an Ok or Err variant from a method:

fn error_if_negative(value: i32) -> Result<(), &'static str> {
    if value < 0 {
        Err("Specified argument was out of the range of valid values. (Parameter 'value')")
    } else {
        Ok(())
    }
}

The panic! macro creates unrecoverable errors:

fn panic_if_negative(value: i32) {
    if value < 0 {
        panic!("Specified argument was out of the range of valid values. (Parameter 'value')")
    }
}

Error propagation

In JavaScript, exceptions are passed up until they are handled or the program terminates. In Rust, unrecoverable errors behave similarly, but handling them is uncommon.

Recoverable errors, however, need to be propagated and handled explicitly. Their presence is always indicated by the Rust function or method signature. Catching an exception allows you to take action based on the presence or absence of an error in JavaScript:

//JavaScript doesn't have a file system in it. People often implement file systems using the BrowserFS library that mimic Node.js APIs.
function write() {
    try {
        fs.writeFileSync('file.txt', 'content');
    } catch (error) {
        console.log('Writing to file failed.');
    }
}

In Rust, this is roughly equivalent to:

fn write() {
    match std::fs::File::create("temp.txt")
        .and_then(|mut file| std::io::Write::write_all(&mut file, b"content"))
    {
        Ok(_) => {}
        Err(_) => println!("Writing to file failed."),
    };
}

Frequently, recoverable errors need only be propagated instead of being handled. For this, the method signature needs to be compatible with the types of the propagated error. The ? operator propagates errors ergonomically:

fn write() -> Result<(), std::io::Error> {
    let mut file = std::fs::File::create("file.txt")?;
    std::io::Write::write_all(&mut file, b"content")?;
    Ok(())
}

Note: to propagate an error with the question mark operator the error implementations need to be compatible, as described in a shortcut for propagating errors. The most general "compatible" error type is the error trait object Box<dyn Error>.

Stack traces

For unrecoverable errors in Rust, panic! Backtraces offer a similar behavior.

Recoverable errors in stable Rust do not yet support Backtraces, but it is currently supported in experimental Rust when using the provide method.

Nullability and Optionality

In JavaScript, null is often used to represent a value that is missing, absent or logically uninitialized. For example:

let some = 1;
let none = null;

Rust has no null and consequently no nullable context to enable. Optional or missing values are instead represented by Option<T>. The equivalent of the JavaScript code above in Rust would be:

let some: Option<i32> = Some(1);
let none: Option<i32> = None;

Option<T> in Rust is practically identical to 'T option from F#.

Control flow with optionality

In JavaScript, you may have been using if/else statements for controlling the flow when using nullable values.

let max = 10;
if (max !== null && max !== undefined) {
    let someMax = max;
    console.log(`The maximum is ${someMax}.`); // Output:The maximum is 10.
}

You can use pattern matching to achieve the same behavior in Rust:

let max = Some(10u32);
match max {
    Some(max) => println!("The maximum is {}.", max), // The maximum is 10.
    None => ()
}

It would even be more concise to use if let:

let max = Some(10u32);
if let Some(max) = max {
    println!("The maximum is {}.", max); // The maximum is 10.
}

Null-conditional operators

The null-conditional operators (?.) make dealing with null in JavaScript more ergonomic. In Rust, they are best replaced by using the map method. The following snippets show the correspondence:

let some = "Hello, World!";
let none = null;
console.log(some?.length); // 13
console.log(none?.length); // undefined
let some: Option<String> = Some(String::from("Hello, World!"));
let none: Option<String> = None;
println!("{:?}", some.map(|s| s.len())); // Some(13)
println!("{:?}", none.map(|s| s.len())); // None

Null-coalescing operator

The null-coalescing operator (??) is typically used to default to another value when a nullable is null:

let some = 1;
let none = null;
console.log(some ?? 0); // 1
console.log(none ?? 0); // 0

In Rust, you can use unwrap_or to get the same behavior:

let some: Option<i32> = Some(1);
let none: Option<i32> = None;
println!("{:?}", some.unwrap_or(0)); // 1
println!("{:?}", none.unwrap_or(0)); // 0

Note: If the default value is expensive to compute, you can use unwrap_or_else instead. It takes a closure as an argument, which allows you to lazily initialize the default value.

Null-forgiving operator

In Rust,

there is no need to use a substitute for it.

Discards

Discards express to the compiler and others to ignore the results (or parts) of an expression.

There are multiple contexts where to apply this, for example as a basic example, to ignore the result of an expression. JavaScript doesn't have discards, but you can call a function without assigning a value to any variable to emulate discards. In JavaScript this looks like:

city.getCityInformation(cityName);

In Rust, ignoring the result of an expression looks identical:

_ = city.get_city_information(city_name);

Discards are also applied for deconstructing "tuples" in JavaScript:

const [_, second] = ["first", "second"];

and, identically, in Rust:

let (_, second) = ("first", "second");

In addition to destructuring tuples, Rust offers destructuring of structs and enums using .., where .. stands for the remaining part of a type:

struct Point {
    x: i32,
    y: i32,
    z: i32,
}

let origin = Point { x: 0, y: 0, z: 0 };

match origin {
    Point { x, .. } => println!("x is {}", x), // x is 0
}

When pattern matching, it is often useful to discard or ignore part of a matching expression. But since there are no discards in JavaScript, and the switch statement of js cannot be used in the same way as rust, you have to emulate this feature in an awkward way:

const _ = ("first", "second");
const result = (_ => {
    switch(true) {
        case _.includes("first"):
            return "first element matched";
        default:
            return "first element did not match";
    }
})();

console.log(result);

and again, in Rust:

_ = match ("first", "second")
{
    ("first", _) => "first element matched",
    (_, _) => "first element did not match"
};

Conversion and Casting

Rust is statically-typed at compile time. Hence, after a variable is declared, assigning a value of a value of a different type (unless it's implicitly convertible to the target type) to the variable is prohibited. There are several ways to convert types in Rust.

Implicit conversions

Implicit conversions exist in JavaScript as well as in Rust (called type coercions). Consider the following example:

let intNumber = 1;
let longNumber = intNumber;

Rust is much more restrictive with respect to which type coercions are allowed:

let int_number: i32 = 1;
let long_number: i64 = int_number; // error: expected `i64`, found `i32`

An example for a valid implicit conversion using subtyping is:

fn bar<'a>() {
    let s: &'static str = "hi";
    let t: &'a str = s;
}

See also:

Explicit conversions

If converting could cause a loss of information, JavaScript requires explicit conversions using a casting expression:

let a = 1.2;
let b = parseInt(a);

Explicit conversions can potentially fail at run-time with exceptions when down-casting.

Rust does not provide coercion between primitive types, but instead uses explicit conversion using the as keyword (casting). Casting in Rust will not cause a panic.

let int_number: i32 = 1;
let long_number: i64 = int_number as _;

Custom conversion

In Rust, the standard library contains an abstraction for converting a value into a different type, in form of the From trait and its reciprocal, Into. When implementing From for a type, a default implementation for Into is automatically provided (called blanket implementation in Rust). The following example illustrates two of such type conversions:

fn main() {
    let my_id = MyId("id".into()); // `into()` is implemented automatically due to the `From<&str>` trait implementation for `String`.
    println!("{}", String::from(my_id)); // This uses the `From<MyId>` implementation for `String`.
}

struct MyId(String);

impl From<MyId> for String {
    fn from(MyId(value): MyId) -> Self {
        value
    }
}

See also:

Operator overloading

JavaScript doesn't support operator overloading. Consider the following example in JavaScript:

class Fraction {
    constructor(numerator, denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    static add(a, b) {
        return new Fraction(a.numerator * b.denominator + b.numerator * a.denominator, a.denominator * b.denominator);
    }

    toString() {
        return `${this.numerator}/${this.denominator}`;
    }
}

console.log(Fraction.add(new Fraction(5, 4), new Fraction(1, 2)).toString());  // Output: "14/8"

In Rust, many operators can be overloaded via traits. This is possible because operators are syntactic sugar for method calls. For example, the + operator in a + b calls the add method (see operator overloading):

use std::{fmt::{Display, Formatter, Result}, ops::Add};

struct Fraction {
    numerator: i32,
    denominator: i32,
}

impl Display for Fraction {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.write_fmt(format_args!("{}/{}", self.numerator, self.denominator))
    }
}

impl Add<Fraction> for Fraction {
    type Output = Fraction;

    fn add(self, rhs: Fraction) -> Fraction {
        Fraction {
            numerator: self.numerator * rhs.denominator + rhs.numerator * self.denominator,
            denominator: self.denominator * rhs.denominator,
        }
    }
}

fn main() {
    println!(
        "{}",
        Fraction { numerator: 5, denominator: 4 } + Fraction { numerator: 1, denominator: 2 }
    ); // 14/8
}

Documentation Comments

A third-party tool called JSDoc provides a mechanism to document the API for types using a comment syntax. JSDoc includes a Markdown plugin that automatically converts Markdown-formatted text to HTML. The comment contains structured data representing the comments and the API signatures. Other tools can process that output to provide human-readable documentation in a different form. A simple example in JavaScript:

public class MyClass {}
/**
 * This is a document comment for `MyClass`.
 * @class
 */
class MyClass {}

In Rust doc comments provide the equivalent to JSDoc documentation comments. Documentation comments in Rust use Markdown syntax. rustdoc is the documentation compiler for Rust code and is usually invoked through cargo doc, which compiles the comments into documentation. For example:

/// This is a doc comment for `MyStruct`.
struct MyStruct;

In JSDoc, the equivalent to cargo doc is jsdoc.

See also:

Memory Management

Rust has memory-safety to avoid a whole class of bugs related to memory access, and which end up being the source of many security vulnerabilities in software. However, Rust can guarantee memory-safety at compile-time; there is no run-time making checks. The one exception here is array bound checks that are done by the compiled code at run-time, be that the Rust compiler. It is possible to write unsafe code in Rust, and in fact, both languages even share the same keyword, literally unsafe, to mark functions and blocks of code where memory-safety is no longer guaranteed.

Rust has no garbage collector (GC). All memory management is entirely the responsibility of the developer. That said, safe Rust has rules around ownership that ensure memory is freed as soon as it's no longer in use (e.g. when leaving the scope of a block or a function). The compiler does a tremendous job, through (compile-time) static analysis, of helping manage that memory through ownership rules. If violated, the compiler rejects the code with a compilation error.

In JavaScript, there is no concept of ownership of memory beyond the GC roots (static fields, local variables on a thread's stack, CPU registers, handles, etc.). It is the GC that walks from the roots during a collection to detemine all memory in use by following references and purging the rest. When designing types and writing code, a JavaScript developer can remain oblivious to ownership, memory management and even how the garbage collector works for the most part, except when performance-sensitive code requires paying attention to the amount and rate at which objects are being allocated on the heap. In contrast, Rust's ownership rules require the developer to explicitly think and express ownership at all times and it impacts everything from the design of functions, types, data structures to how the code is written. On top of that, Rust has strict rules about how data is used such that it can identify at compile-time, data race conditions as well as corruption issues (requiring thread-safety) that could potentially occur at run-time. This section will only focus on memory management and ownership.

There can only be one owner of some memory, be that on the stack or heap, backing a structure at any given time in Rust. The compiler assigns lifetimes and tracks ownership. It is possible to pass or yield ownership, which is called moving in Rust. These ideas are briefly illustrated in the example Rust code below:

#![allow(dead_code, unused_variables)]

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let a = Point { x: 12, y: 34 }; // point owned by a
    let b = a;                      // b owns the point now
    println!("{}, {}", a.x, a.y);   // compiler error!
}

The first statement in main will allocate Point and that memory will be owned by a. In the second statement, the ownership is moved from a to b and a can no longer be used because it no longer owns anything or represents valid memory. The last statement that tries to print the fields of the point via a will fail compilation. Suppose main is fixed to read as follows:

fn main() {
    let a = Point { x: 12, y: 34 }; // point owned by a
    let b = a;                      // b owns the point now
    println!("{}, {}", b.x, b.y);   // ok, uses b
}   // point behind b is dropped

Note that when main exits, a and b will go out of scope. The memory behind b will be released by virtue of the stack returning to its state prior to main being called. In Rust, one says that the point behind b was dropped. However, note that since a yielded its ownership of the point to b, there is nothing to drop when a goes out of scope.

A struct in Rust can define code to execute when an instance is dropped by implementing the Drop trait.

Rust has the notion of a global lifetime denoted by 'static, which is a reserved lifetime specifier. A very rough approximation in C# would be static read-only fields of types.

In JavaScript, references are shared freely without much thought so the idea of a single owner and yielding/moving ownership may seem very limiting in Rust, but it is possible to have shared ownership in Rust using the smart pointer type Rc; it adds reference-counting. Each time the smart pointer is cloned, the reference count is incremented. When the clone drops, the reference count is decremented. The actual instance behind the smart pointer is dropped when the reference count reaches zero. These points are illustrated by the following examples that build on the previous:

#![allow(dead_code, unused_variables)]

use std::rc::Rc;

struct Point {
    x: i32,
    y: i32,
}

impl Drop for Point {
    fn drop(&mut self) {
        println!("Point dropped!");
    }
}

fn main() {
    let a = Rc::new(Point { x: 12, y: 34 });
    let b = Rc::clone(&a); // share with b
    println!("a = {}, {}", a.x, a.y); // okay to use a
    println!("b = {}, {}", b.x, b.y);
}

// prints:
// a = 12, 34
// b = 12, 34
// Point dropped!

Note that:

  • Point implements the drop method of the Drop trait and prints a message when an instance of a Point is dropped.

  • The point created in main is wrapped behind the smart pointer Rc and so the smart pointer owns the point and not a.

  • b gets a clone of the smart pointer that effectively increments the reference count to 2. Unlike the earlier example, where a transferred its ownership of point to b, both a and b own their own distinct clones of the smart pointer, so it is okay to continue to use a and b.

  • The compiler will have determined that a and b go out of scope at the end of main and therefore injected calls to drop each. The Drop implementation of Rc will decrement the reference count and also drop what it owns if the reference count has reached zero. When that happens, the Drop implementation of Point will print the message, “Point dropped!” The fact that the message is printed once demonstrates that only one point was created, shared and dropped.

Rc is not thread-safe. For shared ownership in a multi-threaded program, the Rust standard library offers Arc instead. The Rust language will prevent the use of Rc across threads.

In Rust, the kind of type (basically `enum` or `struct` _in Rust_), does not determine where the backing memory will eventually live. By default, it is always on the stack, but just the way .NET and C# have a notion of boxing value types, which copies them to the heap, the way to allocate a type on the heap is to box it using [`Box`][box.rs]:
let stack_point = Point { x: 12, y: 34 };
let heap_point = Box::new(Point { x: 12, y: 34 });

Like Rc and Arc, Box is a smart pointer, but unlike Rc and Arc, it exclusively owns the instance behind it. All of these smart pointers allocate an instance of their type argument T on the heap.

The new keyword in JavaScript creates an instance of a "type", and while members such as Box::new and Rc::new that you see in the examples may seem to have a similar purpose, new has no special designation in Rust. It's merely a coventional name that is meant to denote a factory. In fact they are called associated functions of the type, which is Rust's way of saying static methods.

Resource Management

Previous section on memory management explains the differences between JavaScript and Rust when it comes to GC, ownership and finalizers. It is highly recommended to read it.

This section is limited to providing an example of a fictional database connection involving a SQL connection to be properly closed/disposed/dropped.

class DatabaseConnection {
    constructor(connectionString) {
        this.connectionString = connectionString;
    }

    closeConnection() {
        // Implementation to close the connection
    }
}

// ...closing connection...
DatabaseConnection.prototype.close = function() {
    this.closeConnection();
    console.log(`Closing connection: ${this.connectionString}`);
};

// Create instances of DatabaseConnection
const db1 = new DatabaseConnection("Server=A;Database=DB1");
const db2 = new DatabaseConnection("Server=A;Database=DB2");

// ...code for making use of the database connection...
// "Dispose" of "db1" and "db2" when their scope ends
struct DatabaseConnection(&'static str);

impl DatabaseConnection {
    // ...functions for using the database connection...
}

impl Drop for DatabaseConnection {
    fn drop(&mut self) {
        // ...closing connection...
        self.close_connection();
        // ...printing a message...
        println!("Closing connection: {}", self.0)
    }
}

fn main() {
    let _db1 = DatabaseConnection("Server=A;Database=DB1");
    let _db2 = DatabaseConnection("Server=A;Database=DB2");
    // ...code for making use of the database connection...
} // "Dispose" of "db1" and "db2" called here; when their scope ends
In Rust, the compiler ensures at compile-time that attempting to use an object after disposing it will typically cause errors cannot happen.

Threading

The Rust standard library supports threading, synchronisation and concurrency. Also the language itself and the standard library do have basic support for the concepts, a lot of additional functionality is provided by crates and will not be covered in this document.

JavaScript is a single-threaded scripting language that does not support multithreading.

Below is a simple JavaScript program that creates a thread (where the thread prints some text to standard output) indirectly by using worker and then waits for it to end:

// Equivalent JavaScript code using Web Workers
const worker = new Worker(URL.createObjectURL(new Blob([`
    self.onmessage = function(event) {
        console.log(event.data);
    };
`], { type: 'application/javascript' })));

worker.postMessage('Hello from a thread!');

The same code in Rust would be as follows:

use std::thread;

fn main() {
    let thread = thread::spawn(|| println!("Hello from a thread!"));
    thread.join().unwrap(); // wait for thread to finish
}

Creating and initializing a thread object and starting a thread are two different actions in JavaScript whereas in Rust both happen at the same time with thread::spawn.

In JavaScript, it's possible to send data as an argument to a thread:

const workerCode = `
self.onmessage = function(e) {
    let eventData = e.data;
    eventData += (" World!");
    console.log("Phrase: " + eventData);
};
`;

const blob = new Blob([workerCode], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));

const data = "Hello";
worker.postMessage(data);

In Rust, there is no variation of thread::spawn that does the same. Instead, the data is passed to the thread via a closure:

use std::thread;

fn main() {
    let data = String::from("Hello");
    let handle = thread::spawn(move || {
        let mut data = data;
        data.push_str(" World!");
        data
    });
    println!("Phrase: {}", handle.join().unwrap());
}

A few things to note:

  • The move keyword is required to move or pass the ownership of data to the closure for the thread. Once this is done, it's no longer legal to continue to use the data variable of main, in main. If that is needed, data must be copied or cloned (depending on what the type of the value supports).

  • Rust thread can return values, which becomes the return value of the join method.

  • It is possible to also pass data to the JavaScript thread via a closure, like the Rust example, but the JavaScript version does not need to worry about ownership since the memory behind the data will be reclaimed by the GC once no one is referencing it anymore.

Synchronization

When data is shared between threads, one needs to synchronize read-write access to the data in order to avoid corruption. In JavaScript:

let data = 0;
let workers = [];
let completedWorkers = 0;

for (let i = 0; i < 10; i++) {
    let worker = new Worker('data:text/javascript,' + encodeURIComponent(`
        let partialData = 0;
        for (let j = 0; j < 1000; j++) {
            partialData++;
        }
        self.postMessage(partialData);
    `));
    
    worker.onmessage = function(event) {
        data += event.data;
        completedWorkers++;
        if (completedWorkers === 10) {
            console.log(data);
            workers.forEach(function(worker) {
                worker.terminate();
            });
        }
    };
    
    workers.push(worker);
    worker.postMessage(null);
}

In Rust, one must make explicit use of concurrency structures like Mutex:

use std::thread;
use std::sync::{Arc, Mutex};

fn main() {
    let data = Arc::new(Mutex::new(0)); // (1)

    let mut threads = vec![];
    for _ in 0..10 {
        let data = Arc::clone(&data); // (2)
        let thread = thread::spawn(move || { // (3)
            for _ in 0..1000 {
                let mut data = data.lock().unwrap();
                *data += 1; // (4)
            }
        });
        threads.push(thread);
    }

    for thread in threads {
        thread.join().unwrap();
    }

    println!("{}", data.lock().unwrap());
}

A few things to note:

  • Since the ownership of the Mutex instance and in turn the data it guards will be shared by multiple threads, it is wrapped in an Arc (1). Arc provides atomic reference counting, which increments each time it is cloned (2) and decrements each time it is dropped. When the count reaches zero, the mutex and in turn the data it guards are dropped. This is discussed in more detail in Memory Management).
  • The closure instance for each thread receives ownership (3) of the cloned reference (2). -
  • The pointer-like code that is *data += 1 (4), is not some unsafe pointer access even if it looks like it. It's updating the data wrapped in the mutex guard.

Unlike the C# version, where one can render it thread-unsafe by commenting out the lock statement, the Rust version will refuse to compile if it's changed in any way (e.g. by commenting out parts) that renders it thread-unsafe. This demonstrates that writing thread-safe code is the developer's responsibility in C# and .NET by careful use of synchronized structures whereas in Rust, one can rely on the compiler. The compiler is able to help because data structures in Rust are marked by special traits (see Interfaces): Sync and Send. Sync indicates that references to a type's instances are safe to share between threads. Send indicates it's safe to instances of a type across thread boundaries. For more information, see the “Fearless Concurrency” chapter of the Rust book.

Producer-Consumer

The producer-consumer pattern is very common to distribute work between threads where data is passed from producing threads to consuming threads without the need for sharing or locking.

const workerCode = `
    self.onmessage = function() {
        const messages = [];
        for (let n = 1; n < 10; n++) {
            messages.push("Message #" + n);
        }
        self.postMessage(messages);
    };
`;

const blob = new Blob([workerCode], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));

// The main thread acts as a consumer here
worker.onmessage = function(event) {
    const messages = event.data;
    messages.forEach(message => console.log(message));
};

// Start the worker
worker.postMessage(null);

The same can be done in Rust using channels. The standard library primarily provides mpsc::channel, which is a channel that supports multiple producers and a single consumer. A rough translation of the above C# example in Rust would look as follows:

The same can be done in Rust using channels. The standard library primarily provides mpsc::channel, which is a channel that supports multiple producers and a single consumer. A rough translation of the above C# example in Rust would look as follows:

use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    let producer = thread::spawn(move || {
        for n in 1..10 {
            tx.send(format!("Message #{}", n)).unwrap();
        }
    });

    // main thread is the consumer here
    for received in rx {
        println!("{}", received);
    }

    producer.join().unwrap();
}

The equivalent of the async-friendly channels in the Rust space is offered by the Tokio runtime.

Testing

Test organization

JavaScript solutions use separate projects to host test code, irrespective of the test framework being used (Jest, Mocha, etc.) and the type of tests (unit or integration) being written. The test code therefore lives in a separate assembly than the application or library code being tested. In Rust, it is a lot more conventional for unit tests to be found in a separate test sub-module (conventionally) named tests, but which is placed in same source file as the application or library module code that is the subject of the tests. This has two benefits:

  • The code/module and its unit tests live side-by-side.

  • There is no need for a workaround like [InternalsVisibleTo] that exists in JavaScript because the tests have access to internals by virtual of being a sub-module.

The test sub-module is annotated with the #[cfg(test)] attribute, which has the effect that the entire module is (conditionally) compiled and run only when the cargo test command is issued.

Within the test sub-modules, test functions are annotated with the #[test] attribute.

Integration tests are usually in a directory called tests that sits adjacent to the src directory with the unit tests and source. cargo test compiles each file in that directory as a separate crate and run all the methods annotated with #[test] attribute. Since it is understood that integration tests in the tests directory, there is no need to mark the modules in there with the #[cfg(test)] attribute.

See also:

Running tests

As simple as it can be, the equivalent of dotnet test in Rust is cargo test.

The default behavior of cargo test is to run all the tests in parallel, but this can be configured to run consecutively using only a single thread:

cargo test -- --test-threads=1

For more information, see "Running Tests in Parallel or Consecutively".

Output in Tests

For very complex integration or end-to-end test, developers sometimes log what's happening during a test. The actual way they do this varies with each test framewor Rust, one simply writes to the standard output using println!. The output captured during the running of the tests is not shown by default unless cargo test is run the with --show-output option:

cargo test --show-output

For more information, see "Showing Function Output".

Assertions

JavaScript users have multiple ways to assert, depending on the framework being used. For example, an assertion in Jest might look like:

test('something has the right length', () => {
    let value = "something";
    expect(value.length).toBe(9);
});

An example that only uses vanilla JavaScript:

function somethingIsTheRightLength() {
    let value = "something";
    console.assert(value.length === 9);
}

somethingIsTheRightLength();

Rust does not require a separate framework or crate. The standard library comes with built-in macros that are good enough for most assertions in tests:

Below is an example of assert_eq in action:

#[test]
fn something_is_the_right_length() {
    let value = "something";
    assert_eq!(9, value.len());
}

The standard library does not offer anything in the direction of data-driven tests, such as [Theory] in xUnit.net.

Mocking

There are crates for Rust too, like [`mockall`][mockall], that can help with mocking. However, it is also possible to use [conditional compilation] by making use of the [`cfg` attribute][cfg-attribute] as a simple means to mocking without needing to rely on external crates or frameworks. The `cfg` attribute conditionally includes the code it annotates based on a configuration symbol, such as `test` for testing. This is not very different to using `DEBUG` to conditionally compile code specifically for debug builds. One downside of this approach is that you can only have one implementation for all tests of the module.

When specified, the #[cfg(test)] attribute tells Rust to compile and run the code only when executing the cargo test command, which behind-the-scenes executes the compiler with rustc --test. The opposite is true for the #[cfg(not(test))] attribute; it includes the annotated only when testing with cargo test.

The example below shows mocking of a stand-alone function var_os from the standard that reads and returns the value of an environment variable. It conditionally imports a mocked version of the var_os function used by get_env. When built with cargo build or run with cargo run, the compiled binary will make use of std::env::var_os, but cargo test will instead import tests::var_os_mock as var_os, thus causing get_env to use the mocked version during testing:

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

/// Utility function to read an environmentvariable and return its value If
/// defined. It fails/panics if the valus is not valid Unicode.
pub fn get_env(key: &str) -> Option<String> {
    #[cfg(not(test))]                 // for regular builds...
    use std::env::var_os;             // ...import from the standard library
    #[cfg(test)]                      // for test builds...
    use tests::var_os_mock as var_os; // ...import mock from test sub-module

    let val = var_os(key);
    val.map(|s| s.to_str()     // get string slice
                 .unwrap()     // panic if not valid Unicode
                 .to_owned())  // convert to "String"
}

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

    pub(crate) fn var_os_mock(key: &str) -> Option<OsString> {
        match key {
            "FOO" => Some("BAR".into()),
            _ => None
        }
    }

    #[test]
    fn get_env_when_var_undefined_returns_none() {
        assert_eq!(None, get_env("???"));
    }

    #[test]
    fn get_env_when_var_defined_returns_some_value() {
        assert_eq!(Some("BAR".to_owned()), get_env("FOO"));
    }
}

Code coverage

There is sophisticated tooling for JavaScript when it comes to analyzing test code coverage, such as Jest.

Rust is providing built-in code coverage implementations for collecting test code coverage.

There are also plug-ins available for Rust to help with code coverage analysis. It's not seamlessly integrated, but with some manual steps, developers can analyze their code in a visual way.

The combination of Coverage Gutters plug-in for Visual Studio Code and Tarpaulin allows visual analysis of the code coverage in Visual Studio Code. Coverage Gutters requires an LCOV file. Other tools besides Tarpaulin can be used to generate that file. Once setup, run the following command:

cargo tarpaulin --ignore-tests --out Lcov

This generates an LCOV Code Coverage file. Once Coverage Gutters: Watch is enabled, it will be picked up by the Coverage Gutters plug-in, which will show in-line visual indicators about the line coverage in the source code editor.

Note: The location of the LCOV file is essential. If a workspace (see Project Structure) with multiple packages is present and a LCOV file is generated in the root using --workspace, that is the file that is being used - even if there is a file present directly in the root of the package. It is quicker to isolate to the particular package under test rather than generating the LCOV file in the root.

Benchmarking

Running benchmarks in Rust is done via cargo bench, a specific command for cargo which is executing all the methods annotated with the #[bench] attribute. This attribute is currently unstable and available only for the nightly channel.

.NET users can make use of Benchmark.js library to benchmark methods and track their performance. The equivalent of Benchmark.js is a crate named Criterion.

As per its documentation, Criterion collects and stores statistical information from run to run and can automatically detect performance regressions as well as measuring optimizations.

Using Criterion is possible to use the #[bench] attribute without moving to the nightly channel.

`Criterion`, in fact, supports multiple output formats, amongst which there is also the `bencher` format, mimicking the nightly `libtest` benchmarks and compatible with the above mentioned action.

Logging and Tracing

For most cases, console.log() is a good default choice for JavaScript, since it works with a variety of built-in and third-party logging providers. In JavaScript, a minimal example for structured logging could look like:

let day = "Thursday";
console.log("Hello ", day); // Hello Thursday.

In Rust, a lightweight logging facade is provided by log. It has less features than ILogger, e.g. as it does not yet offer (stable) structured logging or logging scopes. For something with more feature parity to .NET, Tokio offers tracing. tracing is a framework for instrumenting Rust applications to collect structured, event-based diagnostic information. tracing_subscriber can be used to implement and compose tracing subscribers. The same structured logging example from above with tracing and tracing_subscriber looks like:

fn main() {
    // install global default ("console") collector.
    tracing_subscriber::fmt().init();
    tracing::info!("Hello {Day}.", Day = "Thursday"); // Hello Thursday.
}

OpenTelemetry offers a collection of tools, APIs, and SDKs used to instrument, generate, collect, and export telemetry data based on the OpenTelemetry specification. At the time of writing, the OpenTelemetry Logging API is not yet stable and the Rust implementation does not yet support logging, but the tracing API is supported.

Conditional Compilation

Both JavaScript and Rust are providing the possibility for compiling specific code based on external conditions.

JavaScript doesn't support conditional compilation natively. However, it is possible to use some third-party tool like babel-plugin-preprocessor in order to control conditional compilation.

//#if DEBUG
    console.log("Debug");
//#else
    console.log("Not debug");
//#endif

An example that uses vanilla JavaScript:

let isDebug = true;

if(isDebug)
{
    window.eval(`
    console.log("Debug");
    `);
} else {
    window.eval(`
    console.log("Not debug");
    `);
}

In addition to predefined symbols, it is also possible to use the compiler option DefineConstants to define symbols that can be used with #if, #else, #elif and #endif to compile source files conditionally.

In Rust it is possible to use the cfg attribute, the cfg_attr attribute or the cfg macro to control conditional compilation

The cfg attribute is requiring and evaluating a ConfigurationPredicate

use std::fmt::{Display, Formatter};

struct MyStruct;

// This implementation of Display is only included when the OS is unix but foo is not equal to bar
// You can compile an executable for this version, on linux, with 'rustc main.rs --cfg foo=\"baz\"'
#[cfg(all(unix, not(foo = "bar")))]
impl Display for MyStruct {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str("Running without foo=bar configuration")
    }
}

// This function is only included when both unix and foo=bar are defined
// You can compile an executable for this version, on linux, with 'rustc main.rs --cfg foo=\"bar\"'
#[cfg(all(unix, foo = "bar"))]
impl Display for MyStruct {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str("Running with foo=bar configuration")
    }
}

// This function is panicking when not compiled for unix
// You can compile an executable for this version, on windows, with 'rustc main.rs'
#[cfg(not(unix))]
impl Display for MyStruct {
    fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result {
        panic!()
    }
}

fn main() {
    println!("{}", MyStruct);
}

The cfg_attr attribute conditionally includes attributes based on a configuration predicate.

#[cfg_attr(feature = "serialization_support", derive(Serialize, Deserialize))]
pub struct MaybeSerializableStruct;

// When the `serialization_support` feature flag is enabled, the above will expand to:
// #[derive(Serialize, Deserialize)]
// pub struct MaybeSerializableStruct;

The built-in cfg macro takes in a single configuration predicate and evaluates to the true literal when the predicate is true and the false literal when it is false.

if cfg!(unix) {
  println!("I'm running on a unix machine!");
}

See also:

Features

Conditional compilation is also helpful when there is a need for providing optional dependencies. With cargo "features", a package defines a set of named features in the [features] table of Cargo.toml, and each feature can either be enabled or disabled. Features for the package being built can be enabled on the command-line with flags such as --features. Features for dependencies can be enabled in the dependency declaration in Cargo.toml.

See also:

Environment and Configuration

Accessing environment variables

JavaScript doesn't provide access to environment variables natively. However, some non-browser JavaScript runtimes, such as Node.js and Node provides access to environment variables.

In Node.js:

const name = "EXAMPLE_VARIABLE";

let value = process.env[name];
if (!value) {
    console.log(`Variable '${name}' not set.`);
} else {
    console.log(`Variable '${name}' set to '${value}'.`);
}

In Deno:

const name = "EXAMPLE_VARIABLE";

let value = Deno.env.get(name);
if (!value) {
    console.log(`Variable '${name}' not set.`);
} else {
    console.log(`Variable '${name}' set to '${value}'.`);
}

Rust is providing the same functionality of accessing an environment variable at runtime via the var and var_os functions from the std::env module.

var function is returning a Result<String, VarError>, either returning the variable if set or returning an error if the variable is not set or it is not valid Unicode.

var_os has a different signature giving back an Option<OsString>, either returning some value if the variable is set, or returning None if the variable is not set. An OsString is not required to be valid Unicode.

use std::env;


fn main() {
    let key = "ExampleVariable";
    match env::var(key) {
        Ok(val) => println!("{key}: {val:?}"),
        Err(e) => println!("couldn't interpret {key}: {e}"),
    }
}
use std::env;

fn main() {
    let key = "ExampleVariable";
    match env::var_os(key) {
        Some(val) => println!("{key}: {val:?}"),
        None => println!("{key} not defined in the enviroment"),
    }
}

Rust is also providing the functionality of accessing an environment variable at compile time. The env! macro from std::env expands the value of the variable at compile time, returning a &'static str. If the variable is not set, an error is emitted.

use std::env;

fn main() {
    let example = env!("ExampleVariable");
    println!("{example}");
}

Configuration

JavaScript doesn't support configurations.

In Rust it is available via use of third-party crates such as figment or config. See the following example making use of config crate:

use config::{Config, Environment};

fn main() {
    let builder = Config::builder().add_source(Environment::default());

    match builder.build() {
        Ok(config) => {
            match config.get_string("examplevar") {
                Ok(v) => println!("{v}"),
                Err(e) => println!("{e}")
            }
        },
        Err(_) => {
            // something went wrong
        }
    }
}

LINQ

This section discusses LINQ within the context and for the purpose of querying or transforming sequences and typically collections like lists, sets and dictionaries.

Enumerable items

The equivalent of enumerable items in Rust is IntoIterator. An implementation of IntoIterator::into_iter returns an Iterator. However, when it's time to iterate over the items of a container advertising iteration support through the said types, both languages offer syntactic sugar in the form of looping constructs for iteratables. In JavaScript, there is forEach:

let values = [1, 2, 3, 4, 5];
let output = '';

values.forEach((value, index) => {
    if (index > 0)
        output += ', ';
    output += value;
});

console.log(output); // Outputs: 1, 2, 3, 4, 5

In Rust, the equivalent is simply for:

use std::fmt::Write;

fn main() {
    let values = [1, 2, 3, 4, 5];
    let mut output = String::new();

    for value in values {
        if output.len() > 0 {
            output.push_str(", ");
        }
        // ! discard/ignore any write error
        _ = write!(output, "{value}");
    }

    println!("{output}");  // Prints: 1, 2, 3, 4, 5
}

The for loop over an iterable essentially gets desuraged to the following:

use std::fmt::Write;

fn main() {
    let values = [1, 2, 3, 4, 5];
    let mut output = String::new();

    let mut iter = values.into_iter();      // get iterator
    while let Some(value) = iter.next() {   // loop as long as there are more items
        if output.len() > 0 {
            output.push_str(", ");
        }
        _ = write!(output, "{value}");
    }

    println!("{output}");
}

Rust's ownership and data race condition rules apply to all instances and data, and iteration is no exception. So while looping over an array might look straightforward, one has to be mindful about ownership when needing to iterate the same collection/iterable more than once. The following example iteraters the list of integers twice, once to print their sum and another time to determine and print the maximum integer:

fn main() {
    let values = vec![1, 2, 3, 4, 5];

    // sum all values

    let mut sum = 0;
    for value in values {
        sum += value;
    }
    println!("sum = {sum}");

    // determine maximum value

    let mut max = None;
    for value in values {
        if let Some(some_max) = max { // if max is defined
            if value > some_max {     // and value is greater
                max = Some(value)     // then note that new max
            }
        } else {                      // max is undefined when iteration starts
            max = Some(value)         // so set it to the first value
        }
    }
    println!("max = {max:?}");
}

However, the code above is rejected by the compiler due to a subtle difference: values has been changed from an array to a Vec<int>, a vector, which is Rust's type for growable arrays. The first iteration of values ends up consuming each value as the integers are summed up. In other words, the ownership of each item in the vector passes to the iteration variable of the loop: value. Since value goes out of scope at the end of each iteration of the loop, the instance it owns is dropped. Had values been a vector of heap-allocated data, the heap memory backing each item would get freed as the loop moved to the next item. To fix the problem, one has to request iteration over shared references via &values in the for loop. As a result, value ends up being a shared reference to an item as opposed to taking its ownership.

Below is the updated version of the previous example that compiles. The fix is to simply replace values with &values in each of the for loops.

fn main() {
    let values = vec![1, 2, 3, 4, 5];

    // sum all values

    let mut sum = 0;
    for value in &values {
        sum += value;
    }
    println!("sum = {sum}");

    // determine maximum value

    let mut max = None;
    for value in &values {
        if let Some(some_max) = max { // if max is defined
            if value > some_max {     // and value is greater
                max = Some(value)     // then note that new max
            }
        } else {                      // max is undefined when iteration starts
            max = Some(value)         // so set it to the first value
        }
    }
    println!("max = {max:?}");
}

The ownership and dropping can be seen in action even with values being an array instead of a vector. Consider just the summing loop from the above example over an array of a structure that wraps an integer:

struct Int(i32);

impl Drop for Int {
    fn drop(&mut self) {
        println!("{} dropped", self.0)
    }
}

fn main() {
    let values = [Int(1), Int(2), Int(3), Int(4), Int(5)];
    let mut sum = 0;

    for value in values {
        sum += value.0;
    }

    println!("sum = {sum}");
}

Int implements Drop so that a message is printed when an instance get dropped. Running the above code will print:

value = Int(1)
Int(1) dropped
value = Int(2)
Int(2) dropped
value = Int(3)
Int(3) dropped
value = Int(4)
Int(4) dropped
value = Int(5)
Int(5) dropped
sum = 15

It's clear that each value is acquired and dropped while the loop is running. Once the loop is complete, the sum is printed. If values in the for loop is changed to &values instead, like this:

for value in &values {
    // ...
}

then the output of the program will change radically:

value = Int(1)
value = Int(2)
value = Int(3)
value = Int(4)
value = Int(5)
sum = 15
Int(1) dropped
Int(2) dropped
Int(3) dropped
Int(4) dropped
Int(5) dropped

This time, values are acquired but not dropped while looping because each item doesn't get owned by the interation loop's variable. The sum is printed once the loop is done. Finally, when the values array that still owns all the the Int instances goes out of scope at the end of main, its dropping in turn drops all the Int instances.

These examples demonstrate that while iterating collection types may seem to have a lot of parallels between Rust and JavaScript, from the looping constructs to the iteration abstractions, there are still subtle differences with respect to ownership that can lead to the compiler rejecting the code in some instances.

See also:

Operators

JavaScript doesn't natively support LINQ, but there is a project called LINQ.js that implements LINQ in C# for JavaScript.

Operators in LINQ are implemented in the form of LINQ.js extension methods that can be chained together to form a set of operations, with the most common forming a query over some sort of data source. LINQ.js also offers a SQL-inspired query syntax with clauses like from, where, select, join and others that can serve as an alternative or a companion to method chaining. Many imperative loops can be re-written as much more expressive and composable queries in LINQ.

Rust does not offer anything like LINQ.js's query syntax. It has methods, called adapters in Rust terms, over iterable types and therefore directly comparable to chaining of methods in LINQ.js. However, whlie rewriting an imperative loop as LINQ code in LINQ.js is often beneficial in expressivity, robustness and composability, there is a trade-off with performance. Compute-bound imperative loops usually run faster because they can be optimised by the JIT compiler and there are fewer virtual dispatches or indirect function invocations incurred. The surprising part in Rust is that there is no performance trade-off between choosing to use method chains on an abstraction like an iterator over writing an imperative loop by hand. It's therefore far more common to see the former in code.

The following table lists the most common LINQ methods and their approximate counterparts in Rust.

LINQ.jsRustNote
aggregatereduceSee note 1.
aggregatefoldSee note 1.
allall
anyany
concatchain
countcount
elementAtnth
groupBy-
lastlast
maxmax
maxmax_by
maxBymax_by_key
minmin
minmin_by
minBymin_by_key
reverserev
selectmap
selectenumerate
selectManyflat_map
selectManyflatten
sequenceEqualeq
singlefind
singleOrDefaulttry_find
skipskip
skipWhileskip_while
sumsum
taketake
takeWhiletake_while
toArraycollectSee note 2.
toDictionarycollectSee note 2.
toListcollectSee note 2.
wherefilter
zipzip
  1. The Aggregate overload not accepting a seed value is equivalent to reduce, while the Aggregate overload accepting a seed value corresponds to fold.

  2. collect in Rust generally works for any collectible type, which is defined as a type that can initialize itself from an iterator (see FromIterator). collect needs a target type, which the compiler sometimes has trouble inferring so the turbofish (::<>) is often used in conjunction with it, as in collect::<Vec<_>>(). This is why collect appears next to a number of LINQ extension methods that convert an enumerable/iterable source to some collection type instance.

The following example shows how similar transforming sequences in LINQ.js is to doing the same in Rust. First in LINQ.js:

let result = Enumerable.range(0, 10)
    .where(x => x % 2 === 0)
    .selectMany(x => Enumerable.Range(0, x))
    .aggregate(0, (acc, x) => acc + x);

console.log(result); // Output: 50

And in Rust:

let result = (0..10)
    .filter(|x| x % 2 == 0)
    .flat_map(|x| (0..x))
    .fold(0, |acc, x| acc + x);

println!("{result}"); // 50

Deferred execution (laziness)

Many operators in LINQ are designed to be lazy such that they only do work when absolutely required. This enables composition or chaining of several operations/methods without causing any side-effects.

Rust iterators have the same concept of laziness and streaming.

In both cases, this allows infinite sequences to be represented, where the underlying sequence is infinite, but the developer decides how the sequence should be terminated. The following example shows this in JavaScript:

function* infiniteRange() {
    for (let i = 0; ; ++i) {
        yield i;
    }
}

for (let x of infiniteRange()) {
    if (x < 5) {
        console.log(x); // Prints "0 1 2 3 4"
    } else {
        break;
    }
}

Rust supports the same concept through infinite ranges:

// Generators and yield in Rust are unstable at the moment, so
// instead, this sample uses Range:
// https://doc.rust-lang.org/std/ops/struct.Range.html

for value in (0..).take(5) {
    print!("{value} "); // Prints "0 1 2 3 4"
}

Iterator Methods (yield)

JavaScript has the yield keword that enables the developer to quickly write an iterator method. Coroutines, as they're called in Rust, are still considered an unstable feature at the time of this writing.

Meta Programming

Metaprogramming can be seen as a way of writing code that writes/generates other code.

JavaScript has the concept of metaprogramming, but it refers to intercepting and defining basic language operations, which is different from metaprogramming in C# or Rust. There is a JavaScript source generator called hygen, but it does not call itself a "metaprogramming tool".

Rust is also providing a feature for metaprogramming: macros. There are declarative macros and procedural macros.

Declarative macros allow you to write control structures that take an expression, compare the resulting value of the expression to patterns, and then run the code associated with the matching pattern.

The following example is the definition of the println! macro that it is possible to call for printing some text println!("Some text")

macro_rules! println {
    () => {
        $crate::print!("\n")
    };
    ($($arg:tt)*) => {{
        $crate::io::_print($crate::format_args_nl!($($arg)*));
    }};
}

To learn more about writing declarative macros, refer to the Rust reference chapter macros by example or The Little Book of Rust Macros.

Procedural macros are different than declarative macros. Those accept some code as an input, operate on that code, and produce some code as an output.

Rust does not support reflection.

Function-like macros

Function-like macros are in the following form: function!(...)

The following code snippet defines a function-like macro named print_something, which is generating a print_it method for printing the "Something" string.

In the lib.rs:

extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro]
pub fn print_something(_item: TokenStream) -> TokenStream {
    "fn print_it() { println!(\"Something\") }".parse().unwrap()
}

In the main.rs:

use replace_crate_name_here::print_something;
print_something!();

fn main() {
    print_it();
}

Derive macros

Derive macros can create new items given the token stream of a struct, enum, or union. An example of a derive macro is the #[derive(Clone)] one, which is generating the needed code for making the input struct/enum/union implement the Clone trait.

In order to understand how to define a custom derive macro, it is possible to read the rust reference for derive macros

Attribute macros

Attribute macros define new attributes which can be attached to rust items. While working with asynchronous code, if making use of Tokio, the first step will be to decorate the new asynchronous main with an attribute macro like the following example:

#[tokio::main]
async fn main() {
    println!("Hello world");
}

In order to understand how to define a custom derive macro, it is possible to read the rust reference for attribute macros

Asynchronous Programming

Both JavaScript and Rust support asynchronous programming models, which look similar to each other with respect to their usage. The following example shows, on a very high level, how async code looks like in JavaScript:

async function printDelayed(message, cancellationToken) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    return `Message: ${message}`;
}

Rust code is structured similarly. The following sample relies on async-std for the implementation of sleep:

use std::time::Duration;
use async_std::task::sleep;

async fn format_delayed(message: &str) -> String {
    sleep(Duration::from_secs(1)).await;
    format!("Message: {}", message)
}
  1. The Rust async keyword transforms a block of code into a state machine that implements a trait called Future. In both languages, this allows for writing asynchronous code sequentially.

  2. Note that for both Rust and JavaScript, asynchronous methods/functions are prefixed with the async keyword, but the return types are different. Asynchronous methods in JavaScript indicate the full and actual return type because it can vary. In Rust, it is enough to specify the inner type String because it's always some future; that is, a type that implements the Future trait.

  3. The await keywords are in different positions in JavaScript and Rust. In C#, Promise is awaited by prefixing the expression with await. In Rust, suffixing the expression with the .await keyword allows for method chaining, even though await is not a method.

See also:

Executing tasks

From the following example the PrintDelayed method executes, even though it is not awaited:

let cancellationToken = undefined; 
printDelayed("message", cancellationToken); // Prints "message" after a second.
await new Promise(resolve => setTimeout(resolve, 2000));

async function printDelayed(message, cancellationToken) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log(message);
}

In Rust, the same function invocation does not print anything.

use async_std::task::sleep;
use std::time::Duration;

#[tokio::main] // used to support an asynchronous main method
async fn main() {
    print_delayed("message"); // Prints nothing.
    sleep(Duration::from_secs(2)).await;
}

async fn print_delayed(message: &str) {
    sleep(Duration::from_secs(1)).await;
    println!("{}", message);
}

This is because futures are lazy: they do nothing until they are run. The most common way to run a Future is to .await it. When .await is called on a Future, it will attempt to run it to completion. If the Future is blocked, it will yield control of the current thread. When more progress can be made, the Future will be picked up by the executor and will resume running, allowing the .await to resolve (see async/.await).

While awaiting a function works from within other async functions, main is not allowed to be async. This is a consequence of the fact that Rust itself does not provide a runtime for executing asynchronous code. Hence, there are libraries for executing asynchronous code, called async runtimes. Tokio is such an async runtime, and it is frequently used. tokio::main from the above example marks the async main function as entry point to be executed by a runtime, which is set up automatically when using the macro.

Task cancellation

The previous JavaScript examples included passing a CancellationToken to asynchronous methods, as is considered best practice in JavaScript. CancellationTokens can be used to abort an asynchronous operation.

Because futures are inert in Rust (they make progress only when polled), cancellation works differently in Rust. When dropping a Future, the Future will make no further progress. It will also drop all instantiated values up to the point where the future is suspended due to some outstanding asynchronous operation. This is why most asynchronous functions in Rust don't take an argument to signal cancellation, and is why dropping a future is sometimes being referred to as cancellation.

tokio_util::sync::CancellationToken offers an equivalent to the .NET CancellationToken to signal and react to cancellation, for cases where implementing the Drop trait on a Future is unfeasible.

Executing multiple Tasks

In JavaScript, Promise.race and Task.WhenAll are frequently used to handle the execution of multiple tasks.

Promise.race completes as soon as any task completes. Tokio, for example, provides the tokio::select! macro as an alternative for Promise.race, which means to wait on multiple concurrent branches.

const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

const delayMessage = async (delayTime) => {
    await delay(delayTime);
    return `Waited ${delayTime / 1000} second(s).`;
};

const delay1 = delayMessage(1000);
const delay2 = delayMessage(2000);

Promise.race([delay1, delay2]).then(result => {
    console.log(result); // Output: Waited 1 second(s).
});

The same example for Rust:

use std::time::Duration;
use tokio::{select, time::sleep};

#[tokio::main]
async fn main() {
    let result = select! {
        result = delay(Duration::from_secs(2)) => result,
        result = delay(Duration::from_secs(1)) => result,
    };

    println!("{}", result); // Waited 1 second(s).
}

async fn delay(delay: Duration) -> String {
    sleep(delay).await;
    format!("Waited {} second(s).", delay.as_secs())
}

Again, there are crucial differences in semantics between the two examples. Most importantly, tokio::select! will cancel all remaining branches, while Promise.race leaves it up to the user to cancel any in-flight tasks.

Similarly, Promise.all can be replaced with tokio::join!.

Multiple consumers

In JavaScript a Promise can be used across multiple consumers. All of them can await the task and get notified when the task is completed or failed. In Rust, the Future can not be cloned or copied, and awaiting will move the ownership. The futures::FutureExt::shared extension creates a cloneable handle to a Future, which then can be distributed across multiple consumers.

use futures::FutureExt;
use std::time::Duration;
use tokio::{select, time::sleep, signal};
use tokio_util::sync::CancellationToken;

#[tokio::main]
async fn main() {
    let token = CancellationToken::new();
    let child_token = token.child_token();

    let bg_operation = background_operation(child_token);

    let bg_operation_done = bg_operation.shared();
    let bg_operation_final = bg_operation_done.clone();

    select! {
        _ = bg_operation_done => {},
        _ = signal::ctrl_c() => {
            token.cancel();
        },
    }

    bg_operation_final.await;
}

async fn background_operation(cancellation_token: CancellationToken) {
    select! {
        _ = sleep(Duration::from_secs(2)) => println!("Background operation completed."),
        _ = cancellation_token.cancelled() => println!("Background operation cancelled."),
    }
}

Asynchronous iteration

Rust does not yet have an API for asynchronous iteration in the standard library. To support asynchronous iteration, the Stream trait from futures offers a comparable set of functionality.

In JavaScript, writing async iterators has comparable syntax to when writing synchronous iterators:

async function* RangeAsync(start, count) {
    for (let i = 0; i < count; i++) {
        await new Promise(resolve => setTimeout(resolve, i * 1000));
        yield start + i;
    }
}

(async () => {
    for await (const item of RangeAsync(10, 3)) {
        console.log(item + " "); // Prints "10 11 12".
    }
})();

In Rust, there are several types that implement the Stream trait, and hence can be used for creating streams, e.g. futures::channel::mpsc. async-stream offers a set of macros that can be used to generate streams succinctly.

use async_stream::stream;
use futures_core::stream::Stream;
use futures_util::{pin_mut, stream::StreamExt};
use std::{
    io::{stdout, Write},
    time::Duration,
};
use tokio::time::sleep;

#[tokio::main]
async fn main() {
    let stream = range(10, 3);
    pin_mut!(stream); // needed for iteration
    while let Some(result) = stream.next().await {
        print!("{} ", result); // Prints "10 11 12".
        stdout().flush().unwrap();
    }
}

fn range(start: i32, count: i32) -> impl Stream<Item = i32> {
    stream! {
        for i in 0..count {
            sleep(Duration::from_secs(i as _)).await;
            yield start + i;
        }
    }
}

Project Structure

The JavaScript standard does not specify a specification for the structure of the project. Generally speaking, all the files in a JavaScript library are usually placed in a folder named after the library. The following is a common JavaScript project specification:

.
+-- src/
|   +-- project1.js
+-- styles/
|   +-- style1.js
+-- examples/
|   +-- some-example.js
+-- tests/
    +-- some-integration-test.js

Cargo uses the following conventions for the package layout to make it easy to dive into a new Cargo package:

.
+-- Cargo.lock
+-- Cargo.toml
+-- src/
|   +-- lib.rs
|   +-- main.rs
+-- benches/
|   +-- some-bench.rs
+-- examples/
|   +-- some-example.rs
+-- tests/
    +-- some-integration-test.rs
  • Cargo.toml and Cargo.lock are stored in the root of the package.
  • src/lib.rs is the default library file, and src/main.rs is the default executable file (see target auto-discovery).
  • Benchmarks go in the benches directory, integration tests go in the tests directory (see testing, benchmarking).
  • Examples go in the examples directory.
  • There is no separate crate for unit tests, unit tests live in the same file as the code (see testing).

Managing large projects

For very large projects in Rust, Cargo offers workspaces to organize the project. A workspace can help manage multiple related packages that are developed in tandem. Some projects use virtual manifests, especially when there is no primary package.

Managing dependency versions

There is no concept of dependency in the JavaScript standard. However, some JavaScript runtimes, such as Node.js and Deno, have the concept of dependencies. When managing larger projects in Node.js, it may be appropriate to manage the versions of dependencies centrally, using strategies such as [nvm]. Deno uses [dvm]. Cargo introduced [workspace inheritance] to manage dependencies centrally. [nvm]: https://github.com/nvm-sh/nvm [dvm]: https://github.com/justjavac/dvm [workspace inheritance]: https://doc.rust-lang.org/cargo/reference/workspaces.html#the-package-table

Compilation and Building

JavaScript CLI

There is no concept of CLI in the JavaScript standard. People often use non-browser runtimes such as Node.js and Deno to act as CLIs.The equivalent of the JavaScript CLIs in Rust is Cargo (cargo). Both tools are entry-point wrappers that simplify use of other low-level tools. For example, although you could invoke the JavaScript engines, developers tend to use third-party tools such as webpack and vite to build their solution. Similarly in Rust, while you could use the Rust compiler (rustc) directly, using cargo build is generally far simpler.

Building

When building JavaScript, the scripts coming from dependent packages are generally co-located with the project's output assembly. cargo build in Rust compiles the project sources, except the Rust compiler statically links (although there exist other linking options) all code into a single, platform-dependent, binary.

Developers use different ways to prepare a JavaScript executable for distribution, either as a framework-dependent deployment (FDD) or self-contained deployment (SCD). In Rust, there is no way to let the build output already contains a single, platform-dependent binary for each target.

In Rust, the build output is, again, a platform-dependent, compiled library for each library target.

See also:

Dependencies

There is no concept of dependency in the JavaScript standard. However, some JavaScript runtimes, such as Node.js and Deno, have the concept of dependencies. In Node.js and Deno, the contents of a project file (package.json) define the build options and dependencies. In Rust, when using Cargo, a Cargo.toml declares the dependencies for a package. A typical project file will look like:

{
  "name": "your-project-name",
  "version": "1.0.0",
  "description": "Your project description",
  "dependencies": {
    "linq": "4.0.3"
  }
}

The equivalent Cargo.toml in Rust is defined as:

[package]
name = "hello_world"
version = "0.1.0"

[dependencies]
tokio = "1.0.0"

Cargo follows a convention that src/main.rs is the crate root of a binary crate with the same name as the package. Likewise, Cargo knows that if the package directory contains src/lib.rs, the package contains a library crate with the same name as the package.

Packages

There is no concept of packages in the JavaScript standard. However, some JavaScript runtimes, such as Node.js, have the concept of packages. NPM is most commonly used to install packages for Node.js, and various tools supported it. Deno Package Manager(DPM) is an unofficial package manager for Deno that imitates NPM. For example, adding a Node.js package reference with the Node,js CLI will add the dependency to the project file:

npm install linq

In Rust this works almost the same if using Cargo to add packages.

cargo add tokio

The most common package registry for Node.js is npmjs.com whereas Rust packages are usually shared via crates.io.

Static code analysis

ESLint is an analyzer that provide code quality as well as code-style analysis. The equivalent linting tool in Rust is Clippy.

Clippy can fail if the compiler or Clippy emits warnings (cargo clippy -- -D warnings).

There are further static checks to consider adding to a Rust CI pipeline: