Rust is gaining crazy popularity, and it's easy to see why. As a fast and safe alternative to C++, it offers developers the perfect blend of performance and reliability. With its strong emphasis on memory safety and zero-cost abstractions, Rust has become a go-to language for those seeking to build efficient, high-performance applications without sacrificing code stability.
Recently, I decided to dive into Rust, and I quickly discovered just how powerful and versatile it can be. Rust is not just another programming language β it's a practical tool that strikes a perfect balance between performance, safety, and modern development practices.
To test my newfound interest, I decided to build a CLI application from scratch. This hands-on approach allowed me to explore Rust's features, such as its strong type system, ownership model, and robust ecosystem.
In this post, I'll share my experience diving into Rust β what I've learned, when it's best to use Rust, and how it stands out from the languages I'm already familiar with. I'll also walk you through the process of building my Task Manager CLI application, highlighting the unique features of Rust that made the development both challenging and rewarding. Along the way, I'll compare Rust to other languages I've worked with, offering insights into its strengths, limitations, and where it truly shines. Whether you're considering learning Rust or curious about its practical applications, I hope this post gives you a clearer picture of its potential and why it's worth exploring.
Introduction
Rust's journey began in 2006 when Graydon Hoare, a Mozilla employee, started working on the language as a personal project. His goal was to create a programming language that combined high performance with memory safety β addressing the issues he encountered with existing systems languages like C and C++. Mozilla recognized the potential of the project and officially sponsored its development in 2009.
In 2010, Rust made its first public appearance when Mozilla announced the language. It aimed to create a safer alternative to C++ while maintaining low-level control over hardware. Rust's development continued with contributions from the open-source community, and by 2015, the language reached its first stable release: Rust 1.0. This milestone marked Rust as production-ready and set the stage for its rapid growth.
The Main Purposes Behind Rust
Rust was designed to address three core challenges in software development:
- Memory Safety Without Garbage Collection Many programming errors stem from unsafe memory management, such as null pointer dereferences and buffer overflows. Rust's ownership model enforces strict compile-time checks, ensuring memory safety without relying on a garbage collector.
 - High Performance Like C and C++, Rust allows developers to write code that runs close to the hardware, making it ideal for performance-critical applications. Its zero-cost abstractions ensure that high-level code doesn't come with runtime overhead.
 - Concurrency and Parallelism Rust's ownership and type system prevent common issues like data races, making it a safer choice for building concurrent and parallel systems. Its unique guarantees make it easier to write multithreaded applications without fear of subtle bugs.
 
Why Rust Stands Out
Rust has become the go-to language for a variety of domains:
- Systems Programming: Operating systems, browser engines, and embedded devices.
 - WebAssembly: Rust's performance and safety make it ideal for WebAssembly projects.
 - Command-Line Tools: Lightweight and fast CLI applications with robust error handling.
 - Networking: High-performance servers and applications in industries like finance and gaming.
 
Now, I'll compare Rust to the languages I'm already familiar with: C#, JavaScript, and Go. Let's dive into how they stack up!
Comparison of Rust, Go, C#, and JavaScript
1. Performance
- Rust: Near-C++ performance with zero-cost abstractions and no garbage collection.
 - Go: Performs well for server-side tasks but relies on garbage collection, leading to occasional pauses and overhead.
 - C#: Also garbage-collected, with good performance for high-level applications but unsuitable for systems-level programming or real-time use cases.
 - JavaScript: Runs in a managed runtime (V8 engine, Node.js). Good for web applications but not designed for high-performance tasks.
 
Winner: Rust, for raw performance and systems-level control.
2. Memory Safety
- Rust: Provides compile-time guarantees for memory safety using its ownership and borrowing model, eliminating issues like null dereferences and data races.
 - Go: Garbage collection prevents some memory issues, but it doesn't enforce safety at the level of Rust.
 - C#: Managed memory with garbage collection reduces risks but still allows null references, a common source of bugs.
 - JavaScript: Automatic memory management (garbage collection) ensures safety at a high level but does not guarantee deeper control.
 
Winner: Rust, for its unmatched memory safety without runtime costs.
3. Concurrency
- Rust: Safe and explicit concurrency model with compile-time checks to prevent data races. Requires careful planning but ensures reliability.
 - Go: Concurrency is a core feature, with lightweight goroutines and channels making it easy to write concurrent code. However, it doesn't enforce thread safety at compile time.
 - C#: Provides async/await for high-level asynchronous programming, but thread safety relies on careful use of synchronization primitives.
 - JavaScript: Event-driven concurrency model via async/await and the event loop. Great for I/O-heavy tasks but not suitable for multithreading.
 
Winner: Go, for simplicity and ease of use in concurrency, but Rust offers safer, more robust concurrency for advanced use cases.
4. Ecosystem and Tooling
- Rust: Cargo offers a streamlined experience for dependency management and testing, but the ecosystem is still maturing.
 - Go: A rich standard library, excellent tooling (e.g., 
go fmt,go build), and a mature ecosystem focused on server-side and cloud-native development. - C#: Backed by Microsoft's .NET ecosystem, with extensive libraries and powerful IDEs like Visual Studio for enterprise-level development.
 - JavaScript: The largest ecosystem of libraries and frameworks, thanks to npm and its dominance in web development.
 
Winner: It depends on the use case. Go and C# have strong ecosystems for backend work, JavaScript dominates web development, and Rust is solid but still growing.
5. Learning Curve
- Rust: Steep learning curve due to the ownership and borrowing model, but results in safer and more reliable code.
 - Go: Minimalist syntax and design make it very beginner-friendly, prioritizing simplicity over complexity.
 - C#: Relatively easy for beginners, with strong tooling and abstractions, but its features and runtime behavior can overwhelm new developers.
 - JavaScript: Easy to learn the basics but notoriously inconsistent and challenging to master due to quirks in the language and ecosystem.
 
Winner: Go, for its simplicity and accessibility.
Overall
- Rust: Best for performance, memory safety, and systems-level programming. Ideal for developers who need control and reliability.
 - Go: Best for simplicity and rapid development of server-side and cloud-native applications. Perfect for teams prioritizing productivity.
 - C#: Best for enterprise solutions, desktop applications, and game development in the .NET ecosystem. Strong tooling but less versatile outside its niche.
 - JavaScript: Best for web development and frontend work.
 
Each language has its sweet spot, but Rust stands out for its unmatched safety and performance, while Go excels in developer-friendly concurrency. C# dominates enterprise and game development, and JavaScript is the go-to for web and cross-platform solutions.
Now, let me walk you through how I built a simple Task Manager CLI!
Task Manager CLI
Objectives
- Add New Tasks Allow users to create new tasks with details such as a description, priority, and due date.
 - List Tasks Display all tasks in an organized and user-friendly format, with options to filter by priority, status, or due date.
 - Delete Tasks Enable users to remove tasks from the list once they are no longer needed.
 - Persist Data Save tasks to a persistent storage so that the task list is preserved between application sessions.
 - Command-Line Interface Create an intuitive CLI with clear commands and options, making it easy for users to interact with the application.
 - Error Handling Implement robust error handling to manage invalid inputs or other unexpected behaviors gracefully.
 - Learn Rust Concepts Use the project as a practical exercise to understand Rust's ownership model, struct types, error handling, and modular programming.
 
Design
From the start, I decided to leverage third-party libraries rather than reinvent the wheel for CLI handling, async operations, and database connectivity. For data persistence, I chose to use a MySQL database instead of file storage to practice database interaction with Rust, adding another layer of complexity and realism to the project.
Here's the list of third-party libraries I used:
- clap: A powerful library for parsing command-line arguments and building user-friendly CLI applications. With clap, I easily defined commands, subcommands, and options for the task manager.
 - tokio: An asynchronous runtime for Rust. I used tokio to manage database interactions.
 - sqlx: A lightweight and asynchronous ORM (Object-Relational Mapper) for interacting with MySQL.
 - thiserror: A library for creating custom error types with minimal boilerplate.
 
Let's start with the definition of a program entry point:
use clap::{Arg, Command};
use commands::{add, error::AppError, list, remove};
mod commands;
mod models;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let matches = Command::new("Task Manager")
        .version("1.0")
        .author("Your Name <youremail@example.com>")
        .about("Manage your tasks from the command line")
        .subcommand(
            Command::new("add")
                .about("Adds a new task")
                .arg(Arg::new("title").required(true))
                .arg(Arg::new("description").required(false)),
        )
        .subcommand(
            Command::new("remove")
                .about("Removes a task by index")
                .arg(Arg::new("index").required(true)),
        )
        .subcommand(Command::new("list").about("Lists all tasks"))
        .get_matches();
    if let Some(matches) = matches.subcommand_matches("add") {
        let title = matches
            .get_one::<String>("title")
            .ok_or_else(|| AppError::ValidationError("Title is required".to_string()))?;
        let description = matches.get_one::<String>("description").map(|d| d.as_str());
        add(title, description).await?;
    } else if let Some(matches) = matches.subcommand_matches("remove") {
        let index = matches
            .get_one::<String>("index")
            .and_then(|i| i.parse::<i64>().ok())
            .ok_or_else(|| AppError::ValidationError("Invalid index".to_string()))?;
        remove(&index).await?;
    } else if let Some(_) = matches.subcommand_matches("list") {
        list().await?;
    }
    Ok(())
}Let's go line-by-line!
Imports of libraries and local modules:
use clap::{Arg, Command};
use commands::{add, error::AppError, list, remove};Declaration of internal modules more details later):
mod commands;
mod models;Main function:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {#[tokio::main]: A procedural macro that marks the entry point of an asynchronous application using thetokioruntime.async fn main: Themainfunction is asynchronous, enabling the use ofawaitfor asynchronous operations.Result<(), Box<dyn std::error::Error>>: The function returns aResult. If successful, it returnsOk(()). If an error occurs, it returns aBoxcontaining any type that implements thestd::error::Errortrait.
Definition of main command and its subcommands:
let matches = Command::new("Task Manager")
        .version("1.0")
        .author("Your Name <youremail@example.com>")
        .about("Manage your tasks from the command line")
        .subcommand(
            Command::new("add")
                .about("Adds a new task")
                .arg(Arg::new("title").required(true))
                .arg(Arg::new("description").required(false)),
        )
        .subcommand(
            Command::new("remove")
                .about("Removes a task by index")
                .arg(Arg::new("index").required(true)),
        )
        .subcommand(Command::new("list").about("Lists all tasks"))
        .get_matches();Command matching:
if let Some(matches) = matches.subcommand_matches("add") {Retrieve the title argument from the matches object:
let title = matches
    .get_one::<String>("title")
    .ok_or_else(|| AppError::ValidationError("Title is required".to_string()))?;Retrieves the optional description argument, converting it to a string slice (&str) if provided:
let description = matches.get_one::<String>("description").map(|d| d.as_str());Calls the add function (likely defined in the commands module) with the provided title and description. The await keyword waits for the asynchronous operation to complete:
add(title, description).await?;Returns Ok(()) if no errors occur:
Ok(())Now let's review the code in add function:
use crate::commands::{conn::connect, error::AppError};
pub async fn add(title: &str, description: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
    // check len between 1 and 255
    if title.is_empty() || title.len() > 255 {
        return Err(Box::new(AppError::ValidationError("Title must be between 1 and 255 characters".to_string())));
    }
    let pool = connect().await?;
    sqlx::query("INSERT INTO tasks (title, description) VALUES (?, ?)")
        .bind(title)
        .bind(description)
        .execute(&pool)
        .await?;
    println!("Task '{}' added!", title);
    Ok(())
}Method connect connects to MySQL database:
use sqlx::{Error, MySql, MySqlPool, Pool};
pub async fn connect() -> Result<Pool<MySql>, Error> {
    MySqlPool::connect("mysql://root:root@localhost:3306/task_manager").await
}I have defined some custom errors:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("Task not found")]
    NotFound,
    #[error("Validation error: {0}")]
    ValidationError(String),
}And Task model itself:
use std::fmt::Display;
#[derive(Debug, sqlx::FromRow)]
pub struct Task {
    pub id: i64,
    pub title: String,
    pub description: Option<String>,
    pub done: bool,
}
impl Display for Task {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let status = if self.done { "Done" } else { "Not done" };
        let description = self.description.as_deref().unwrap_or("None");
        write!(f, "{}. {}: {} ({})", self.id, self.title, description, status)
    }
}To compile and run the program, you'll need to install the Rust toolchain. The easiest way to get started is by following the simple instructions provided at https://rustup.rs/. Rustup is the recommended installer and version manager for Rust.
After it is installed you can run the next command:
cargo run helpYou should see the next output:
Manage your tasks from the command line
Usage: task-manager [COMMAND]
Commands:
  add     Adds a new task
  remove  Removes a task by index
  list    Lists all tasks
  help    Print this message or the help of the given subcommand(s)
Options:
  -h, --help     Print help
  -V, --version  Print versionBefore performing any CRUD operations, ensure the MySQL cluster is running. To start the cluster, execute the following command in the root folder of the project (you can find the project link below):
docker compose up -dAnd finally, you can create some simple task:
cargo run add "Research New Rust Libraries" "Explore libraries for advanced logging in Rust and evaluate which fits the project requirements."Now you should see next:
Task 'Research New Rust Libraries' added!By executing the command cargo run list, you can view the complete list of all tasks:
Tasks:
1. Research New Rust Libraries: Explore libraries for advanced logging in Rust and evaluate which fits the project requirements. (Not done)Application Code
You can find the complete code for the project here:
If you find it helpful, don't forget to star the repository β your support means a lot! π
Conclusion
Building this Task Manager CLI has been an exciting and rewarding journey into the world of Rust. While I'm far from calling myself an expert in Rust, this project has given me a solid foundation in its unique concepts, like ownership, error handling, and async operations.
As I continue to learn Rust, my next challenge will be to develop a web service. I'm fully aware that creating a web service in Rust won't be as straightforward or as efficient (in terms of development speed) as using Go, C#, or JavaScript β languages that have mature ecosystems and tools specifically designed for web development. However, the web service I build with Rust will shine in one key area: performance.
From my perspective, Rust is not just a language; it's a mindset shift. While Rust may not replace more established languages for certain use cases, its strengths make it an incredible tool for performance-critical, scalable applications.
For anyone considering diving into Rust, my advice is simple: start small, embrace the challenges, and enjoy the process. The language might demand more upfront effort, but the skills you gain will transform how you think about programming. I look forward to expanding my knowledge of Rust and sharing more projects in the future. Stay tuned!