The Rust standard library, affectionately called std
, is exceptionally well-designed, but that doesn’t mean it’s perfect.
More experienced Rust developers tend to navigate around some of its sharper parts.
In this article, I want to highlight the areas in std
that I personally avoid.
Keep in mind that this list is subjective, so take it with a grain of salt.
My intention is to point out some pitfalls and suggest alternatives where appropriate.
Threading
Rust’s threading library is quite solid. That said, managing threads can be a bit of a footgun. In particular, forgetting to join a thread can have some unexpected side effects.
use thread;
use Duration;
;
In the above scenario, cleanup tasks (such as flushing caches or closing files) might not get executed.
So, even if you do nothing with the handle, it is still a best practice to join()
it.
For more details on the topic, check out matklad’s article: “Join Your Threads”.
In fact, there was a proposal to make std::thread::JoinHandle
be #[must_use]
, but it was ultimately declined because it would produce too many warnings.
This comment summarized the situation pretty well:
I’d say the key issue here is that
thread::spawn
is the easiest way of spawning threads, but not the best one for casual use. Manually calling.join().unwrap()
is a chore and easy to forget, which makesthread::spawn
a potential footgun.
For new code, I recommend using thread::scope
instead, which is a much better API in every conceivable way.
The documentation addresses the above issue directly:
Unlike non-scoped threads, scoped threads can borrow non-
'static
data, as the scope guarantees all threads will be joined at the end of the scope. All threads spawned within the scope that haven’t been manually joined will be automatically joined before this function returns.
Alternatively, you could use a thread pool library or rayon, in case you have an iterator you want to parallelize without manually managing threads.
std::collections::LinkedList
Implementing a linked list in Rust is not easy. That’s because Rust’s ownership model is detrimental to self-referential data structures.
Some people might not know that the standard library ships an implementation of a linked list at std::collections::LinkedList
.
In all those years, I never felt the urge to use it.
It might even be the least-used collection type in the standard library overall.
For all ordinary use cases, a Vec
is superior and straightforward to use.
Vectors also have better cache locality and performance characteristics: all items are stored in contiguous memory, which is much better for fast memory access.
On the other side, elements in a linked list can be scattered all over the heap.
If you want to learn more, you can read this paper, which contains some benchmarks.
You might be wondering why linked lists get used at all. They have their place as a very specialized data structure that is only really helpful in some resource-constrained or low-level environments like a kernel. The Linux kernel, for example, uses a lot of linked lists. The reason is that the kernel’s intrusive linked list implementation embeds list nodes directly within data structures which is very memory efficient and allows objects to be in multiple lists simultaneously without additional allocations. 1
As for normal, everyday code, just use a Vec
.
Even the documentation of LinkedList
itself agrees:
NOTE: It is almost always better to use
Vec
orVecDeque
because array-based containers are generally faster, more memory efficient, and make better use of CPU cache.
I believe the LinkedList should not have been included in the standard library in the first place. Even its original author agrees.
There are some surprising gaps in the API; for instance, LinkedList::remove
is still a nightly-only feature2:
use LinkedList;
Even if you wanted a linked list, it probably would not be std::collections::LinkedList
:
- It doesn’t support O(1) splice, O(1) node erasure, or O(1) node insertion - only O(1) operations at the list ends
- It has all the disadvantages of a doubly-linked list but none of its advantages
- Custom implementations are often needed anyway. For example, many real-world use cases require an intrusive linked list implementation, not provided by
std
. An intrusive list is what the Linux kernel provides and even Rust for Linux has its own implementation of an intrusive list. - Arena-based linked lists are often needed for better performance.
There is a longer discussion in the Rust forum.
Better implementations exist that provide more of the missing operations expected from a proper linked list implementation:
There’s a thing to be said about BTreeMap
as well, but I leave it at that. 3
Path Handling
Path
does a decent job of abstracting away the underlying file system.
One thing I always disliked was that Path::join
returns a PathBuf
instead of a Result<PathBuf, Error>
.
I mentioned in my ‘Pitfalls of Safe Rust’ article
that Path::join
joining a relative path with an absolute path results in the absolute path being returned.
use Path;
I think that’s pretty counterintuitive and a potential source of bugs.
On top of that, many programs assume paths are UTF-8 encoded and frequently convert them to str
.
That’s always a fun dance:
use Path;
These path.as_os_str().to_str()
operations must be repeated everywhere.
It makes path manipulation every so slightly annoying.
There are a few more issues with paths in Rust:
Path
/OsStr
lacks common string manipulation methods (likefind()
,replace()
, etc.), which makes many common operations on paths quite tedious- The design creates a poor experience for Windows users, with inefficient
Path
/OsStr
handling that doesn’t fit the platform well. It’s a cross-platform compromise, but it creates some real problems.
Of course, for everyday use, Path
is perfectly okay, but if path handling is a core part of your application, you might want to consider using an external crate instead.
camino
is a good alternative crate, which just assumes that paths are UTF-8 (which, in 2025, is a fair assumption).
This way, operations have much better ergonomics.
Platform-Specific Date and Time Handling
In my opinion, it’s actually great to have some basic time functionality right in the standard library.
However, just be aware that std::time::SystemTime
is platform dependent, which causes some headaches.
Same for Instant
, which is a wrapper around the most precise time source on each OS.
Since time is such a thin wrapper around whatever the operating system provides, you can run into some nasty behavior. For example, this does not always result in “1 nanosecond” on Windows:
use ;
The documentation does not specify the clock’s accuracy or how it handles leap seconds, except to note that SystemTime
does not account for them.
If you depend on proper control over time, such as managing leap seconds or cross-platform support, you’re better off using an external crate. For a great overview, see this survey in the Rust forum, titled: ‘The state of time in Rust: leaps and bounds’.
In general, I believe std::time
works well in combination with the rest of the standard library, such as for sleep
:
use thread;
use Duration;
;
sleep
…but apart from that, I don’t use it for much else. If I had to touch any sort of date calculations, I would defer to an external crate
such as chrono
or time
.
Summary
As you can see, my list of warts in the Rust standard library is quite short. Given that Rust 1.0 was released more than a decade ago, the standard library has held up really well. That said, I reserve the right to update this article in case I become aware of additional sharp edges in the future.
In general, I like that Rust has a relatively small standard library because once a feature is in there it stays there forever. 4