For the past 6 8 months, I have been learning Rust. This article is a brain dump of my experience with the language, what I like and donât like, and other miscellaneous thoughts. This is written from the perspective of a long-time JavaScript/TypeScript engineer coming to Rust for a real production system.
Overview
In summer 2025, I started supporting a friend with the server of their mobile game, following the departure of their previous backend engineer. The stack is quite straightforward: a Rust program running on a single powerful machine, with a MongoDB database. Game clients connect via websockets.
Now, itâs important to point out that my background is primarily in frontend. I have done some PHP during my studies back in 2010, and then a lot of Node.js throughout my career. I am by no means experienced with backend technology.
Which means I have (and still do) relied a lot on AI coding agents to support me in my work. I would never have been able to pick up Rust that easily, let alone actually bring value to the system, without Cursor.
You Cargo girl!
Cargo is Rustâs build system and package manager. Itâs responsible for compiling your code, as well as installing dependencies. And let me tell you: it Just Worksâ˘. Itâs incredibly stable, generally fast, and never disappoints. From installing dependencies to compiling the code to using workspaces, everything just works out of the box and without a hiccup. Very refreshing.
The JavaScript ecosystem is often the butt of the joke, with its multiple runtimes (Node.js and deno and Bun), various package managers (npm and pnpm and yarn), many flavours (CJS and UMD and ESM) and countless dependencies⌠And while you learn how to navigate them, itâs always a pain and a time sink.
There are some old JavaScript projects I just donât touch anymore, not because Iâm bored of them, but because I know Iâll need to spend half a day updating 10 different major dependencies and fighting with CJS/ESM compatibility and life is just too short for this.
Embracing the compiler
The first thing that struck me with Rust is that the compiler is very picky. Coming from JavaScript where basically anything goes (even with TypeScript which remains quite loose), it was a bit of a wall for me.
Compilation errors
At first, every line I attempted to change faced me with some kind of error, usually a borrow/lifetime issue. Fortunately, error messages are exceptionally clear, with a stack trace, an error code, a human-friendly explanation, a fix suggestion and a link to the documentation. Itâs such a refreshing change after undefined is not a function.
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:16
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{s1}, world!");
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try </span>rustc <span class="token parameter variable">--explain</span> E0382<span class="token variable">.
error: could not compile </span>ownership<span class="token variable"> (bin "ownership") due to 1 previous error
Still, it can be tricky to understand ownership if you have no experience with that concept whatsoever. The string type is also quite complicated compared to other languages Iâve worked with: there is the mutable String and the literal &str, and which to use and how to convert can be a little unintuitive at first.
On the bright side, if the code ever compiles, then it most likely runs. Runtime failures basically canât really happen, so this was extremely comforting. Knowing that if the code can be compiled and deployed, it will surely run just fine (safe of logic problems) was a great confidence boost in actually getting things done.
Compilation time
I suppose the price to pay to get such a good compiler is time. Every check thatâs shifted left at compilation time adds to, well, compilation time. Our server is relatively small, and our machine is quite powerful, and it can still take several minutes to do a clean build with 8 vCPUs. I can only imagine how long it takes to compile very large Rust applications with many dependencies.
I think I properly realized that when setting up a test server for us. I purchased a rather cheap machine since that server would have essentially no traffic. But when it came to actually compiling and deploying the Rust binary (which happens a lot for testing purposes), it would take 5â10 minutes. Thatâs because the machine I bought had only 2 vCPUs, so compilation could not be parallelized efficiently.
Error handling done right
To begin with, Rust differentiates between ârecoverableâ and âunrecoverableâ errors. This is a very important distinction, as it suggests there are natural errors (or cases) that the program should handle (like a record not being found in the database), and more problematic errors that are symptomatic of actual bugs (like accessing a location beyond the end of an array).
Rust doesnât have exceptions, it has the Result<T, E> type for recoverable errors. This enum has 2 variants: Ok and Err. The Ok variant indicates the operation was successful, and it contains the successfully generated value. The Err variant means the operation failed, and it contains information about how or why the operation failed. From there, we can use the pattern matching (with match) to conditionally handle the error.
In the following example, the File::open(..) function returns a Result enum, containing the file handle if it worked, or an error if it didnât (for instance if the file is missing).
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = match File::open("hello.txt") {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(file_created) => file_created,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
In TypeScript, weâd do something like this (which I find to be âharderâ to write and less readable):
import { readFile, writeFile } from "node:fs/promises"
async function main() {
try {
let greetingFile = await readFile("hello.txt", "utf8")
// Do something with the content
} catch (readError: unknown) {
if ((readError as any)?.code === "ENOENT") {
try {
await writeFile("hello.txt", "")
} catch (writeError) {
throw new Error(</span><span class="token string">Problem creating the file: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">String</span><span class="token punctuation">(</span>writeError<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">)
}
} else {
throw new Error(</span><span class="token string">Problem opening the file: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">String</span><span class="token punctuation">(</span>readError<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">)
}
}
}
Time and time again, I realize how elegant error management is in Rust. Between the Result enum that encapsulates either outcomes, the match keyword, the ? operator shortcut for error propagation, and more⌠Itâs just very well thought out, and it makes complex programs convenient and readable.
Locks and deadlocks
Now, that has been my nemesis. Rust has strong concurrency primitives. And because of that, it needs data structures that can guarantee consistency across multiple threads. There are a couple of them (namely RwLock for âread-write lockâ and Mutex for âmutual exclusionâ), and they rely on a lock, which guards the data it holds and limits access to a single thread at a time. The Rust book has a good metaphor for it:
For a real-world metaphor for a mutex, imagine a panel discussion at a conference with only one microphone. Before a panelist can speak, they have to ask or signal that they want to use the microphone. When they get the microphone, they can talk for as long as they want to and then hand the microphone to the next panelist who requests to speak. If a panelist forgets to hand the microphone off when theyâre finished with it, no one else is able to speak. If management of the shared microphone goes wrong, the panel wonât work as planned!
The problem with these data structures, however necessary they may be, is that you have the potential to put the runtime in a deadlock. In its simplest form, this can happen when:
- Thread A acquires the write lock of a data structure. This blocks all read attempts until the lock is released.
- Thread B attempts to acquire the read lock of the same data structure. Itâs paused until the write lock is released.
- Thread A waits on thread B for any reason (for instance to acquire another lock held by thread B).
In that case, thread A will hold the write lock for as long as it needs, and if it waits on a second thread that is itself waiting on the lock, both threads will end up in an unrecoverable deadlock. Itâs not always a textbook âtwo locks, two threadsâ deadlock, but the end result is the same: no progress and a hung server.
This can absolutely bring your runtime to its knees and make your server hang, rendering it essentially broken even though it actually runs.
Now here is the thing: I know this is a skill issue. This is probably a problem that very experienced programmers no longer face because they have achieved the nirvana of thread-safe concurrency. But for anyone learning Rust on anything non-trivial, this can be a real ass-biting moment.
Whatâs next?
More Rust! To be honest, I could see myself pick it as a language of choice for a production backend project. Itâs definitely a mature and performant language, with incredible developer experience, and a vibrant community.
I guess it depends a lot on the project. After all, Iâm still much more comfortable with TypeScript, but Iâve also written JS my entire career. So if like me youâre coming from JS/TS and would like to try Rust, start with small CLI tools or glue code before jumping into a heavily concurrent server. The compiler will teach you a lot, but locks and deadlocks are still a separate boss fight.