Skip to main content

Rust through the eyes of FE developer

Rust is a programming language that has gained a lot of popularity recently. As someone who has been interested in Rust for a while, I finally decided to play with it for a month.

In this article, I’ll share my experience building a command-line tool with Rust and highlight what I loved about the language, as well as what I struggled with.

I decided to build a command-line tool because it was a perfect fit for my study project.

First, I went through the exercise projects in the official Rust documentation, including Command line apps in Rust and chapters 12 and 20 in the Rust book on building a command-line program and a multithreaded web server, respectively.

Once I finished those tutorials, I started building my own project.

I work a lot with Nx in the terminal, but I don’t always remember all the available commands and projects. So my goal was to build a utility that could show me all the projects in the monorepo and list the available tasks for each selected project. I chose this project for a few reasons:

  • It solved my own problem (I couldn’t find something similar).
  • There were a lot of interesting concepts in Rust that I could use to improve my skills, such as JSON parsing, file manipulation (read and write), and handling user input.
  • It allowed me to get my hands dirty and learn as much as possible.

You can check out the Rx code on my Github, but be warned that it is not production-ready code.

1. Very Clear and Informative Warnings and Errors

Permalink to “1. Very Clear and Informative Warnings and Errors”

One thing I really appreciated about Rust was its clear and informative error messages. Whenever there was a problem with my code, Rust would usually tell me exactly what the problem was and how to fix it. This was a refreshing change from the sometimes cryptic error messages I’m used to seeing in TypeScript.

Exapmple of error messaged shown by rust compiler. Including
precise code location when the error happened. Error message: arguments to this function are incorrect. expected `&mut std::string::String`, found struct `std::string::String`, help: consider mutably borrowing here: `&mut buffer`

Rust’s iterators are much more powerful than those in JavaScript. They are lazy evaluated by default and developers have much more utility functions available. It’s like having Lodash or Ramda as part of the standard library.

In the simple example below, we take first 10 elements from infinit sequence of numbers, use filter function to keep only multiples of 3, multiply them by 3 and sum the resulting numbers into one.

fn main() {
let sequence = 1..;

let res: u8 = sequence
.take(10)
.filter(|x| x % 3 == 0 )
.map(|x| x * 3)
.sum();

assert_eq!(res, 54);
}

Another nice example of iterators can be seen in Rust by example book where it is used to implement the Fibonacci sequence.

A complete list of available methods can be found in the documentation for Iterator.

For JavaScript developers, pattern matching in Rust is like a switch statements on steroids. You’ll find yourself using it a lot to return and transform values based on conditions. Rust will also force you to handle all the branches, which is great for making sure your code is robust and handles all possible scenarios.

let file_result: Result<File> = File::open("nx.json");

match file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => panic!("The file you are looking for does not exists."),
ErrorKind::PermissionDenied => panic!("You shall not pass into this file."),
ErrorKind::InvalidInput => panic!("Bad arguments for the command, doc."),
_ => panic!("Something else went wrong, but I was too lazy to deal with it.")
}
};

There is even JS proposal to add pattern matching into the specification.

Most of the time, Rust will return Option or Result types instead of throwing an error. This is inspired by monads from functional programming and allows you to handle errors in a more controlled and elegant way.

It plays nicely with pattern matching, allowing you to handle various errors separately. This leads you to handle errors by default (but you can bypass this). It also allows you to compose the flow of your program more effectively. As you can see in the previous code example.

Rust’s variant of objects is a struct. A struct can have properties and methods (both static and instance methods) just like JavaScript objects. And if you want to create some generic behavior that can be reused across different structs, you can create a trait.

For example, in my case, I reused a trait for parsing and deserializing JSON from 3rd party library. I simply added a trait to the struct describing my JSON shape (one line of code), and it parsed the JSON file into a strictly-typed struct that I could easily work with. Which is a awesome compared to JS/TS world.

#[derive(Serialize, Deserialize, Debug)]
struct NxProjectFile {
name: String,
tags: Vec<String>,
targets: HashMap<String, Target>,
}

// later in the code
let project_json: NxProjectFile = serde_json::from_reader(project_file_reader).unwrap();

The Rust documentation is excellent and was a huge help as I learned the language. I was able to find most of what I needed in the docs, and most of the time, I read it in my editor as a tooltip when I needed to check how a method works.

I also found the documentation to be really good for the crates I worked with (Rust’s version of NPM packages).

As someone who uses Neovim as their primary text editor, I was happy to find that Rust has great tooling support in Neovim.

I was able to set up my environment quickly and start working with Rust in just a few minutes thanks to this video Rust setup for Neovim

I still feel a little confused about the ownership logic. When you are passing a variable to a lower scope and when you only reference it (function in lower scope will borrow it).

Luckily, Rust will often tell you what to do to fix the errors.

In Rust, every statement must end with a semicolon. Otherwise, the expression is considered a return statement (if it is the last expression in the block scope). As a JavaScript developer, I’m not used to typing semicolons anymore, and Prettier handles this for me. So I often forget to type it which is annoying.

When working with Rust you have to sometimes deal with the low-level nature of the language, which is something I’m not used to as a front-end developer.

For example, I wanted to write to a file and overwrite its content. But instead, Rust did always append the content. After a little googling I found out that when you read from a file first, you have to rewind a pointer back to the beginning of the file. Otherwise write operation will append to content to the file.

I enjoyed the short time with Rust. It was a nice experience, and I had fun learning it. I think it takes the good parts from both object-oriented programming and functional programming.

I will probably return to it in the future to play with it a little bit more. Most likely, to polish the Rx utility or maybe when I need to build another CLI utility or when playing with WebAssembly.

Tomas Pustelnik

Front-end developer with focus on semantic HTML, CSS, performance and accessibility. Fan of great and clever design, tooling addict and neverending learner. Building Qjub in my free time and writing on this blog.