#301 Aug 2026

301. ln_1p / exp_m1 — Adding 1.0 Destroys Your Tiny Float Before ln Ever Runs

(1.0 + x).ln() looks innocent, but for tiny x the 1.0 + step rounds away most of x’s digits before ln even sees them. ln_1p and exp_m1 do the + 1 internally, where nothing is lost.

Say you’re turning a tiny growth rate into a log-return:

1
2
3
4
5
6
let r = 1e-12_f64;

// 1.0 + r rounds away most of r
let naive = (1.0 + r).ln();
// ~1.0000889e-12 — 4 digits, then noise
assert!((naive - r).abs() > 1e-17);

The problem is spacing. Around 1.0, consecutive f64 values are about 2.2e-16 apart, so 1.0 + 1e-12 snaps to the nearest representable value — and the snap error (~9e-17 here) is enormous compared to r itself. ln then faithfully computes the log of the wrong number. You get 1.0000889e-12: four correct digits, then noise.

ln_1p computes ln(1 + x) without ever materializing 1 + x:

1
2
3
let precise = r.ln_1p();
// true value is r - r²/2 ≈ r - 5e-25
assert!((precise - r).abs() < 1e-20);

Full precision — the true answer differs from r only in the 25th decimal place, and ln_1p nails it.

The same trap exists in the other direction. e^x - 1 for tiny x:

1
2
3
4
5
6
7
// exp(r) ≈ 1.0000000000010000...
// subtracting 1.0 exposes the rounding
let naive = r.exp() - 1.0;
assert!((naive - r).abs() > 1e-17);

let precise = r.exp_m1(); // e^r - 1
assert!((precise - r).abs() < 1e-20);

And since they’re exact inverses, they round-trip cleanly:

1
2
let back = r.ln_1p().exp_m1();
assert!((back - r).abs() < 1e-26);

If x is comfortably large — say 0.1 and up — the naive forms are fine. But interest rates, probabilities, and per-step deltas live exactly in the tiny range where they aren’t. Both methods have been stable since Rust 1.0; they’re a rename away.

#300 Aug 2026

300. atan2 — The Angle Formula That Knows Its Quadrant

(dy / dx).atan() happily returns an angle — in the wrong quadrant half the time, and NaN at the origin. atan2 takes both components and gets every case right.

This morning’s bite 299 covered hypot, the robust way to get the length of a vector. Its companion problem is the angle — and the naive formula fails in a sneakier way.

atan alone can only return angles between −π/2 and π/2, because the division throws away the signs of the inputs:

1
2
3
4
5
let (dx, dy) = (-3.0_f64, 4.0);

// (-3, 4) is up-and-left: second quadrant
let naive = (dy / dx).atan();
assert!(naive < 0.0); // ...a negative angle?!

4.0 / -3.0 is the same number as -4.0 / 3.0, so atan can’t tell the second quadrant from the fourth. atan2 takes dy and dx separately, keeps both signs, and returns the full −π to π range:

1
2
3
4
5
6
let angle = dy.atan2(dx);
assert!((angle - 2.2142974355881810).abs() < 1e-12);

// naive answer is off by exactly π
let err = angle - naive;
assert!((err - std::f64::consts::PI).abs() < 1e-12);

It also survives the edge the division dies on — the origin:

1
2
assert!((0.0_f64 / 0.0).atan().is_nan());
assert_eq!(0.0_f64.atan2(0.0), 0.0);

Together with hypot, this is the complete cartesian-to-polar kit, and the round trip closes to within an ulp:

1
2
3
4
let r = dx.hypot(dy);
let theta = dy.atan2(dx);
assert!((r * theta.cos() - dx).abs() < 1e-12);
assert!((r * theta.sin() - dy).abs() < 1e-12);

Argument order trips everyone up once: it’s y.atan2(x) — the vertical component is self. If your angles keep coming out mirrored across the diagonal, you’ve swapped them.

#299 Aug 2026

299. hypot — The Distance Formula That Doesn't Overflow

(dx*dx + dy*dy).sqrt() looks harmless — until the squares overflow to infinity even though the answer would fit in an f64 just fine. hypot computes the same distance without ever squaring your inputs.

The textbook distance formula squares first, adds, then takes the root:

1
2
3
4
let dx = 3.0_f64;
let dy = 4.0_f64;
let dist = (dx * dx + dy * dy).sqrt();
assert_eq!(dist, 5.0);

Works great — until the components get big. f64::MAX is about 1.8e308, so squaring anything past ~1.3e154 blows straight through it:

1
2
3
4
5
6
let dx = 3.0e200_f64;
let dy = 4.0e200_f64;

// dx*dx is 9e400 — that's infinity in f64
let naive = (dx * dx + dy * dy).sqrt();
assert!(naive.is_infinite());

The final answer, 5.0e200, fits in an f64 with room to spare. It’s only the intermediate squares that overflow. hypot is built for exactly this — it computes sqrt(x² + y²) using a rescaling algorithm that never materializes the squares:

1
2
3
let dist = dx.hypot(dy);
assert!(dist.is_finite());
assert!((dist / 5.0e200 - 1.0).abs() < 1e-15);

(Note the relative comparison — hypot is accurate to within an ulp or so, not bit-exact.)

The same trick saves you at the other end of the scale: for tiny components the squares underflow to zero, and the naive formula returns 0.0 for a distance that plainly isn’t zero. hypot gets that case right too:

1
2
3
let tiny = 1.0e-200_f64;
assert_eq!((tiny * tiny).sqrt(), 0.0); // underflow
assert!(tiny.hypot(tiny) > 0.0);

If your coordinates are sane screen pixels, the naive formula is fine. But the moment the magnitudes are user-supplied or physics-scaled, reach for hypot — same one-liner, no cliff at 1e154.

#298 Aug 2026

298. copysign — Stamp One Number's Sign Onto Another, Sign Bit and All

Bite 297 showed signum reading the sign bit. copysign is the other half: it writes one — take this magnitude, give it that number’s sign, one call.

The branch

You have a fixed magnitude and a direction living in another variable — a step size, a force, a correction — and out comes the same conditional every time:

1
2
3
let delta: f64 = -3.2;

let step = if delta < 0.0 { -0.25 } else { 0.25 };

One call

1
2
3
4
5
let step = 0.25_f64.copysign(delta); // -0.25

assert_eq!(3.5_f64.copysign(-1.0), -3.5);
assert_eq!((-3.5_f64).copysign(1.0), 3.5);
assert_eq!(7.0_f64.copysign(2.0), 7.0);

The receiver contributes the magnitude, the argument contributes the sign. Yesterday’s “step toward the target” loop works for floats too — magnitude from you, direction from the gap:

1
2
3
4
5
6
7
let mut pos = 1.0_f64;
let target = 2.0;

while (target - pos).abs() >= 0.25 {
    pos += 0.25_f64.copysign(target - pos);
}
assert_eq!(pos, 2.0); // 0.25 divides 1.0 exactly

It really is the sign bit

Like f64::signum, copysign works on the raw sign bit — which is exactly what makes it more honest than the if:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// the branch calls -0.0 "positive"...
let x = -0.0_f64;
let s = if x < 0.0 { -1.0 } else { 1.0 };
assert_eq!(s, 1.0);

// ...copysign actually looks at the bit
assert!(1.0_f64.copysign(-0.0).is_sign_negative());

// and a NaN's magnitude stays NaN, sign still applied
assert!(f64::NAN.copysign(-1.0).is_sign_negative());

Anywhere you’re bolting a direction onto a magnitude — drag opposing velocity, rounding away from zero, mirroring a coordinate — copysign is the branch-free, -0.0-correct way to say it.

#297 Aug 2026

297. signum — The -1 / 0 / +1 You Keep Building With an If-Else Ladder

Every “which direction?” check ends up as the same three-branch ladder: greater, less, equal. signum is that ladder as one method call — with a float twist you need to know about.

The ladder

You want the direction of a difference — for a comparator, a game step, a sort key:

1
2
3
4
5
6
7
8
9
let delta: i32 = -7;

let dir = if delta > 0 {
    1
} else if delta < 0 {
    -1
} else {
    0
};

One call

1
2
3
4
5
let dir = delta.signum(); // -1

assert_eq!(42i32.signum(), 1);
assert_eq!(0i32.signum(), 0);
assert_eq!((-3i64).signum(), -1);

It shines whenever you need to move one step toward a target, whatever the distance:

1
2
3
4
5
6
7
let mut pos: i32 = 3;
let target = 7;

while pos != target {
    pos += (target - pos).signum(); // steps ±1, stops exactly
}
assert_eq!(pos, target);

No overshoot, no separate branches for “target is behind me” — the sign does the steering.

The float trap

f64::signum is not the same function. It reports the sign bit, so zero is never 0.0:

1
2
3
assert_eq!(0.0_f64.signum(), 1.0);    // not 0.0!
assert_eq!((-0.0_f64).signum(), -1.0);
assert!(f64::NAN.signum().is_nan());

If you want the integer-style three-way answer for floats, be explicit about zero:

1
2
3
let x = 0.0_f64;
let dir = if x == 0.0 { 0.0 } else { x.signum() };
assert_eq!(dir, 0.0);

Reach for signum on integers without a second thought; on floats, remember it answers “which sign bit?” — not “is this positive, negative, or zero?”.

#296 Aug 2026

296. [T; N]::each_ref — Map Over an Array Without Giving It Away

arr.map(f) consumes the array — one call and your data is gone. each_ref() turns [T; N] into [&T; N], so you can map over borrows and keep the original.

Bite 118 showed [T; N]::map — transform an array, stay allocation-free. But it takes the array by value. With non-Copy elements, the array is gone after one call:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let names = [
    String::from("alpha"),
    String::from("beta"),
    String::from("gamma"),
];

let lens = names.map(|s| s.len());
println!("{names:?}");
// error[E0382]: borrow of moved value:
// `names`

each_ref (stable since 1.77) borrows every element in place, producing a [&T; N] — a fixed-size array of references, no allocation, no clone:

1
2
3
4
5
let lens = names.each_ref()
    .map(|s| s.len());

assert_eq!(lens, [5, 4, 5]);
println!("{names:?}"); // still yours

The result is a real array, so everything from bite 118 applies: the output keeps the size N in the type, and map on it can’t fail halfway.

There’s a mutable twin, each_mut, which gives [&mut T; N] — handy when you want to hand out exclusive borrows of each slot separately:

1
2
3
4
5
let mut scores = [1, 2, 3];
let [a, _, c] = scores.each_mut();
*a += 10;
*c += 30;
assert_eq!(scores, [11, 2, 33]);

Destructuring each_mut() is the clean way to get multiple &mut into one array at the same time — no split_at_mut index math, and the borrow checker sees each element as its own borrow.

Why not just names.iter().map(...).collect()? That builds a Vec — a heap allocation, and the length N is erased from the type. each_ref().map(...) stays [_; N] end to end.

#295 Aug 2026

295. String::leak — A &'static str From Runtime Data, Without the Box Detour

An API demands &'static str, but your string is built at runtime. The old trick was Box::leak(s.into_boxed_str()) — since 1.72, String::leak says what you mean.

You’ve built a name at startup and some API insists on &'static str:

1
2
3
4
5
6
fn set_worker_name(name: &'static str) { /* … */ }

let name = format!("worker-{id}");
set_worker_name(&name);
// error[E0597]: `name` does not
// live long enough

The value will live for the rest of the program — the borrow checker just can’t know that. The classic workaround was leaking through a Box (bite 67):

1
2
let name: &'static str =
    Box::leak(name.into_boxed_str());

String::leak (stable since 1.72) does it in one step, straight off the format!:

1
2
3
let name: &'static str =
    format!("worker-{id}").leak();
set_worker_name(name);

It consumes the String and hands back a &'static mut str — mutable, exclusive, and alive until the process exits (it coerces to plain &'static str on the spot, as above):

1
2
3
4
let m: &'static mut str =
    String::from("abc").leak();
m.make_ascii_uppercase();
assert_eq!(m, "ABC");

Two things to keep in mind. First, the allocation is never freed — that’s the point. Do this for once-per-process values (config, names, interned keys), never in a loop or per-request path. Second, unlike into_boxed_str, which shrinks the buffer to fit, leak leaks the whole allocation, spare capacity included. A String with 4 KB of capacity holding 10 bytes leaks 4 KB. If that matters, call shrink_to_fit() first.

#294 Aug 2026

294. AssertUnwindSafe — catch_unwind Won't Touch Your &mut Until You Sign the Waiver

You wrapped the job loop in catch_unwind (bite 291), added a completed-counter — and the build broke: the type `&mut i32` may not be safely transferred across an unwind boundary. AssertUnwindSafe is how you tell the compiler you’ve thought it through.

catch_unwind requires its closure to be UnwindSafe — a marker trait, like Send, that the compiler derives automatically. Capturing a &mut breaks it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::panic;

let mut completed = 0;
for id in 0..4 {
    let result = panic::catch_unwind(|| {
        let v = run_job(id);
        completed += 1; // error[E0277]: the type
        // `&mut i32` may not be safely trans-
        // ferred across an unwind boundary
        v
    });
}

The concern is logical corruption, not memory safety: if the closure panics halfway through updating something it borrowed, you catch the panic and then keep using data that may be half-updated. Types with poisoning like Mutex (bite 290) handle this themselves and stay UnwindSafe; a raw &mut T or RefCell<T> can’t make that promise, so catch_unwind refuses them.

Here the fix is a judgment call, and it’s an easy one — completed += 1 is the last statement, so a panic can’t leave it torn. Wrap the closure in AssertUnwindSafe to vouch for it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::panic::{self, AssertUnwindSafe};

let mut completed = 0;
let mut failed = 0;
for id in 0..4 {
    let result = panic::catch_unwind(
        AssertUnwindSafe(|| {
            let v = run_job(id);
            completed += 1;
            v
        }),
    );
    if result.is_err() {
        failed += 1;
    }
}
assert_eq!(completed, 3);
assert_eq!(failed, 1);

AssertUnwindSafe(x) is a zero-cost wrapper that implements UnwindSafe unconditionally — no unsafe, no runtime check. You can’t cause undefined behavior with it; the worst case is observing state a panic left half-updated, which is exactly what you’re asserting can’t happen (or doesn’t matter).

Rule of thumb: on Err, either discard the state the closure touched or make sure every mutation is panic-proof — then AssertUnwindSafe is a fact, not a wish.

#293 Jul 2026

293. thread::panicking — A Second Panic in Drop Doesn't Unwind, It Aborts

Your Drop impl asserts a cleanup invariant. A test fails, unwinding starts, your destructor runs — and its assert fails too. Two panics at once: the process aborts and the original error message is gone.

Rust can’t unwind twice at the same time. If a destructor panics while the thread is already unwinding from an earlier panic, the runtime gives up and calls abort() — no unwinding, no catch_unwind rescue (bite 291), just a dead process that hides the failure you actually cared about.

The problem:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Guard {
    committed: bool,
}

impl Drop for Guard {
    fn drop(&mut self) {
        // If a panic is already unwinding past this
        // guard, this second panic aborts the process.
        assert!(self.committed, "guard dropped uncommitted");
    }
}

Any panic between creating the guard and committing it now aborts instead of unwinding — your test harness never even gets to print the real failure.

std::thread::panicking() tells you whether the current thread is mid-unwind, so the destructor can stand down:

1
2
3
4
5
6
7
8
impl Drop for Guard {
    fn drop(&mut self) {
        if std::thread::panicking() {
            return; // already unwinding — don't make it worse
        }
        assert!(self.committed, "guard dropped uncommitted");
    }
}

The original panic unwinds cleanly, catch_unwind and your panic hook (bite 292) see it as usual, and the guard still enforces its invariant on every normal exit path.

Rule of thumb: any Drop that can panic — asserts, unwrap, flushing a writer — should either swallow the error or check thread::panicking() first.

#292 Jul 2026

292. panic::set_hook — You Caught the Panic, but It Still Screamed to stderr

catch_unwind gave you a tidy Err — yet the full “thread panicked at …” message hit stderr anyway. std::panic::set_hook owns that output.

Yesterday’s bite kept the worker loop alive with catch_unwind. But run it and stderr is still full of noise, because the default panic hook prints before unwinding even starts:

1
2
3
4
5
6
7
use std::panic;

let _ = panic::catch_unwind(|| run_job(2));
// You handled the Err — but stderr still shows:
// thread 'main' panicked at src/main.rs:4:9:
// job 2 choked
// note: run with `RUST_BACKTRACE=1` ...

The hook and the unwind are separate mechanisms. catch_unwind decides where unwinding stops; the hook decides what gets printed. Replace it and the message is yours — one line, your format, your logger:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::panic;

panic::set_hook(Box::new(|info| {
    let loc = info.location().unwrap();
    let msg = info.payload_as_str().unwrap_or("?");
    eprintln!("[worker] {msg} ({}:{})",
        loc.file(), loc.line());
}));

let _ = panic::catch_unwind(|| run_job(2));
// stderr: [worker] job 2 choked (src/main.rs:4)

info is a PanicHookInfo: location() gives file, line, and column, and payload_as_str() (stable since 1.91) recovers the message when it’s a String or &str — no downcast dance.

Three things to know. The hook is global and process-wide — install it once at startup, not per-job, and note that set_hook swaps the hook for every thread. It runs even under panic = "abort", which makes it the one place to flush logs before the process dies. And take_hook() returns the previous hook as a value, so you can restore the default — or wrap it: grab the old hook, install a closure that logs your line and then calls the old one.

Silencing expected panics in tests is the classic use: swap in a no-op hook, catch_unwind the call you expect to panic, restore with take_hook — assertion output stays clean.