- name
- rust-modern-apis
- description
- Reference for stable Rust APIs added in versions 1.89 through 1.99 (August 2025 - October 2026). Use whenever writing, reviewing, or refactoring Rust code when MSRV allows, to replace patterns that were verbose before newer APIs existed.
- when_to_use
- Modernizing Rust code, upgrading MSRV, "the latest Rust features", "can this be simpler in modern Rust?", or seeing manual UTF-8 truncation, path extension manipulation, advisory file locking via external crates, ignored `retain` removal results, `try_into().unwrap()` for fixed arrays, `compare_exchange` loops for atomic update, `cfg_if` crate usage, matching `0`/`1` to `bool`, `assert!(matches!(...))` in tests, hand-rolled bit-manipulation idioms, chained `strip_prefix`/`strip_suffix`, `.map(f).unwrap_or_default()`, the `itoa` crate, manual UTF-16LE/BE decoding, the `filetime` crate, `from_utf8_lossy(&v).into_owned()`, or `Box::into_raw` + `NonNull::new_unchecked` pairs.
# Modern Rust APIs (1.89 – 1.99)
This skill is a lookup table for stable Rust APIs added after 1.88. Use it when writing or reviewing Rust code — replace older verbose patterns with newer concise ones where the project's MSRV allows.
## How to use this skill
1. **Check the project's MSRV first.** Look at `Cargo.toml` for `rust-version = "X.Y"`. Only suggest APIs available at or below the MSRV. If MSRV is lower than an API's version, either skip the suggestion or note that "raising MSRV to X.Y unlocks this."
2. **Scan the code for trigger patterns** (see section below). Each trigger maps to a newer API. When you see one, suggest the replacement with a brief before/after.
3. **For detailed API info**, read the matching reference file in `references/`. Files are organized by domain, not by version.
4. **Don't over-apply.** Some patterns are fine as-is. Only suggest a change when the new API is clearly better (shorter, safer, or fixes a subtle bug).
## Trigger patterns — fastest lookup
Scan for these code shapes first. Each points to a concrete API that replaces it.
| Code pattern spotted | Modern replacement | Version | Details |
|----------------------|-------------------|---------|---------|
| `Duration::from_secs(60 * N)` or ad-hoc minute math | `Duration::from_mins(N)` | 1.91 | [time.md](references/time.md) |
| `Duration::from_secs(60 * 60 * N)` | `Duration::from_hours(N)` | 1.91 | [time.md](references/time.md) |
| `path.with_extension("X.tmp")` (trying to ADD suffix) | `path.with_added_extension("tmp")` | 1.91 | [paths.md](references/paths.md) |
| Manual `file_stem` + `format!` to compose meta paths | `path.with_added_extension(...)` | 1.91 | [paths.md](references/paths.md) |
| `file_stem()` returning wrong thing for `.tar.gz` | `Path::file_prefix()` | 1.91 | [paths.md](references/paths.md) |
| `path.to_string_lossy() == "literal"` | `path == Path::new("literal")` or `path == "literal"` | 1.91 | [paths.md](references/paths.md) |
| `Ipv4Addr::new(a, b, c, d)` from `[u8; 4]` | `Ipv4Addr::from_octets(bytes)` | 1.91 | [net.md](references/net.md) |
| `Ipv6Addr::new(a, b, c, d, e, f, g, h)` from segments | `Ipv6Addr::from_segments(segs)` | 1.91 | [net.md](references/net.md) |
| Manual `is_char_boundary` loop for UTF-8 truncation | `str::floor_char_boundary(n)` / `ceil_char_boundary(n)` | 1.91 | [strings.md](references/strings.md) |
| `checked_add(x).unwrap()` where overflow = bug | `strict_add(x)` | 1.91 | [arithmetic.md](references/arithmetic.md) |
| `iter.into_iter().chain(other)` as free pattern | `std::iter::chain(iter, other)` | 1.91 | [iterators.md](references/iterators.md) |
| `[val; N]` requiring `Copy` for non-Copy types | `core::array::repeat(val)` | 1.91 | [iterators.md](references/iterators.md) |
| `retain` where you need to see/count removed items | `extract_if(..)` (Vec since 1.87, BTree since 1.91) | 1.87+ | [collections.md](references/collections.md) |
| `fs2::FileExt::try_lock_exclusive` or custom lock | `File::try_lock()` / `File::lock()` | 1.89 | [io-files.md](references/io-files.md) |
| External `pid-lock`/`fd-lock` crate | Built-in `File::lock` | 1.89 | [io-files.md](references/io-files.md) |
| `Result<Result<T, E>, E>` manual flatten | `Result::flatten()` | 1.89 | [results.md](references/results.md) |
| `slice.try_into::<[T; N]>().unwrap()` | `slice.as_array::<N>()` / `as_mut_array::<N>()` | 1.93 | [slices.md](references/slices.md) |
| `slice.windows(N)` with const N giving `&[T]` | `slice.array_windows::<N>()` giving `&[T; N]` | 1.94 | [slices.md](references/slices.md) |
| `write_copy_of_slice` / `write_clone_of_slice` on `MaybeUninit` | Stabilized `<[MaybeUninit<T>]>::write_copy_of_slice` | 1.93 | [maybe-uninit.md](references/maybe-uninit.md) |
| One-off `impl Display` for temporary formatter | `std::fmt::from_fn(|f| ...)` | 1.93 | [formatting.md](references/formatting.md) |
| `pop()` then `if !cond { push_back }` | `pop_front_if(cond)` / `pop_back_if(cond)` | 1.93 (VecDeque), 1.86 (Vec) | [collections.md](references/collections.md) |
| Checking `LazyLock` was initialized without blocking | `LazyLock::get()` / `LazyCell::get()` | 1.94 | [sync.md](references/sync.md) |
| Mutating `LazyLock` after init with `&mut` access | `LazyLock::force_mut()` / `get_mut()` | 1.94 | [sync.md](references/sync.md) |
| `peekable.peek().filter(...).map(...)` with consume | `peekable.next_if_map(|v| ...)` | 1.94 | [iterators.md](references/iterators.md) |
| `duration.as_nanos() > u64::MAX` concerns | `Duration::from_nanos_u128(n)` | 1.93 | [time.md](references/time.md) |
| Magic number `4` for UTF-8 byte length | `char::MAX_LEN_UTF8` | 1.93 | [strings.md](references/strings.md) |
| Magic number `2` for UTF-16 unit length | `char::MAX_LEN_UTF16` | 1.93 | [strings.md](references/strings.md) |
| `BTreeMap`/`BTreeSet` full scan to evict | `BTreeMap::extract_if(..)` / `BTreeSet::extract_if(..)` | 1.91 | [collections.md](references/collections.md) |
| `Default` not implemented for `Pin<Box<T>>` etc | Now works for `Pin<Box<T>>`, `Pin<Rc<T>>`, `Pin<Arc<T>>` | 1.91 | [sync.md](references/sync.md) |
| Unsafe `Box::new(MaybeUninit::zeroed().assume_init())` | `Box::new_zeroed()` (unsafe-free alternative) | 1.92 | [maybe-uninit.md](references/maybe-uninit.md) |
| `compare_exchange_weak` loop for atomic CAS-update pattern | `AtomicPtr/Bool/Isize/Usize::update()` / `try_update()` | 1.95 | [sync.md](references/sync.md) |
| Matching integer `0`/`1` to produce `bool`, or `n != 0` for strict-validation | `bool::try_from(n)` (returns `Err` for any value ≠ 0/1) | 1.95 | [arithmetic.md](references/arithmetic.md) |
| `cfg_if::cfg_if! { ... }` from external crate | `core::cfg_select! { ... }` (std macro) | 1.95 | [changelog.md](references/changelog.md) |
| Manual `#[cold]` helper + `core::hint::unreachable_unchecked` to mark cold branches | `core::hint::cold_path()` | 1.95 | [changelog.md](references/changelog.md) |
| `match` arm with `if let Some(x) = expr.foo() && cond(x)` simulated via nested matches | `match { Pattern if let Some(x) = ... => ... }` (`if let` guards) | 1.95 | [changelog.md](references/changelog.md) |
| `unsafe { &*ptr }` to dereference a pointer known non-null | `unsafe { ptr.as_ref_unchecked() }` (and `as_mut_unchecked`) | 1.95 | [changelog.md](references/changelog.md) |
| `assert!(matches!(x, Pat))` in tests, or `match x { Pat => {} _ => panic!() }` test scaffolding | `assert_matches!(x, Pat)` (prints the value's `Debug` on failure) | 1.96 | [assertions.md](references/assertions.md) |
| `LazyLock::new(\|\| value)` / `LazyCell::new(\|\| value)` where `value` is already computed | `LazyLock::from(value)` / `LazyCell::from(value)` | 1.96 | [sync.md](references/sync.md) |
| `std::ops::Range` field blocking `#[derive(Copy)]` on a struct | `core::range::Range` (is `Copy`; `IntoIterator` not `Iterator`) | 1.96 | [iterators.md](references/iterators.md) |
| `1 << (Self::BITS - 1 - x.leading_zeros())` to isolate the highest set bit | `x.isolate_highest_one()` | 1.97 | [arithmetic.md](references/arithmetic.md) |
| `x & x.wrapping_neg()` to isolate the lowest set bit | `x.isolate_lowest_one()` | 1.97 | [arithmetic.md](references/arithmetic.md) |
| `Self::BITS - x.leading_zeros()` for value bit width | `x.bit_width()` | 1.97 | [arithmetic.md](references/arithmetic.md) |
| Manual math for the index of the highest/lowest set bit | `x.highest_one()` / `x.lowest_one()` | 1.97 | [arithmetic.md](references/arithmetic.md) |
| `s.strip_prefix(p).and_then(\|s\| s.strip_suffix(q))` chain | `s.strip_circumfix(p, q)` (also on slices) | 1.98 | [strings.md](references/strings.md) |
| `haystack.find(part)` to recover the index of a `split()` result | `str::substr_range` / `slice::subslice_range` (pointer-derived, no search) | 1.98 | [strings.md](references/strings.md) |
| `chunks_exact(2).map(u16::from_le_bytes)` + `String::from_utf16` | `String::from_utf16le` / `from_utf16be` (+ `_lossy`) | 1.98 | [strings.md](references/strings.md) |
| `itoa` crate, or `n.to_string()` just to get digits into a buffer | `n.format_into(&mut NumBuffer::new())` | 1.98 | [formatting.md](references/formatting.md) |
| `.map(f).unwrap_or_default()` on `Option`/`Result` | `.map_or_default(f)` | 1.98 | [results.md](references/results.md) |
| `if cond { Ok(()) } else { Err(e) }` or `cond.then_some(()).ok_or(e)` | `cond.ok_or(e)` / `cond.ok_or_else(f)` | 1.98 | [results.md](references/results.md) |
| `unsafe` transmute between `&mut u32` and `&AtomicU32` | `Atomic*::from_mut` / `from_mut_slice` / `get_mut_slice` | 1.98 | [sync.md](references/sync.md) |
View on GitHub