๐Ÿฆ€ Functional Rust

331: Timeouts with time::timeout

Difficulty: 3 Level: Advanced Wrap any async operation with a deadline โ€” if it doesn't complete in time, get a structured error instead of waiting forever.

The Problem This Solves

External services fail in two ways: they return an error, or they go silent. An error you can handle. Silence hangs your service indefinitely โ€” connections pile up, memory grows, users wait. Without timeouts, a slow database, a network partition, or a stuck worker can bring down your entire application. Timeouts are not optional in production code. But adding them naively โ€” with threads, flags, and polling โ€” is error-prone and verbose. You end up with shared `AtomicBool` cancelled flags, checking them periodically, and still having race conditions. You need a clean abstraction that says "try this, give up after N milliseconds." The second problem is error types: when a timeout fires, you need to distinguish it from the operation actually failing. "We never got a response" and "the operation returned an error" are different situations requiring different handling (retry vs. fail fast).

The Intuition

In async Rust with tokio: `tokio::time::timeout(Duration::from_millis(100), some_future).await` โ€” that's it. Returns `Ok(value)` if the future completes in time, or `Err(Elapsed)` if not. This example uses `mpsc::recv_timeout` as the synchronous analogy โ€” the same "try to get a result, give up after this long" pattern, just with channels instead of futures.
Python asyncio:    asyncio.wait_for(coro, timeout=1.0)
JavaScript:        Promise.race([fetch(...), delay(1000).then(() => { throw new Error('timeout') })])
Rust (tokio):      timeout(Duration::from_secs(1), async_operation()).await
Rust (std/sync):   rx.recv_timeout(Duration::from_secs(1))
The Rust version has a structural advantage: the compiler forces you to handle both cases. You can't accidentally ignore the timeout.

How It Works in Rust

#[derive(Debug)]
enum TimeoutError {
 Elapsed,                  // operation took too long
 TaskFailed(String),       // operation ran but returned an error
}

fn with_timeout<T: Send + 'static>(
 timeout: Duration,
 f: impl FnOnce() -> Result<T, String> + Send + 'static
) -> Result<T, TimeoutError> {
 let (tx, rx) = mpsc::channel();
 thread::spawn(move || { let _ = tx.send(f()); });  // run in background

 match rx.recv_timeout(timeout) {
     Ok(Ok(v))  => Ok(v),                                           // success
     Ok(Err(e)) => Err(TimeoutError::TaskFailed(e)),                // task failed
     Err(mpsc::RecvTimeoutError::Timeout) => Err(TimeoutError::Elapsed),  // too slow
     Err(mpsc::RecvTimeoutError::Disconnected) => Err(TimeoutError::TaskFailed("disconnected".into())),
 }
}
The `let _ = tx.send(f())` in the background thread: if the timeout fires and the receiver is dropped, `send` will return `Err`. We ignore it โ€” the work can finish or not, we've already moved on. For async code, `tokio::time::timeout` works the same way but cancels the future properly instead of letting the thread continue.

What This Unlocks

Key Differences

ConceptOCamlRust
Operation timeout`Lwt_unix.with_timeout secs f``tokio::time::timeout(dur, fut).await`
Sync timeout`Thread.delay` + shared flag`rx.recv_timeout(dur)`
Timeout result typeexception `Lwt_unix.Timeout``Err(Elapsed)` โ€” matches cleanly
Distinguish timeout vs errorexception handlersseparate enum variants in `Result`
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

#[derive(Debug)]
enum TimeoutError { Elapsed, TaskFailed(String) }

impl std::fmt::Display for TimeoutError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Elapsed => write!(f, "operation timed out"),
            Self::TaskFailed(e) => write!(f, "task failed: {e}"),
        }
    }
}

fn with_timeout<T: Send + 'static>(timeout: Duration, f: impl FnOnce()->Result<T,String>+Send+'static) -> Result<T, TimeoutError> {
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || { let _ = tx.send(f()); });
    match rx.recv_timeout(timeout) {
        Ok(Ok(v)) => Ok(v),
        Ok(Err(e)) => Err(TimeoutError::TaskFailed(e)),
        Err(mpsc::RecvTimeoutError::Timeout) => Err(TimeoutError::Elapsed),
        Err(mpsc::RecvTimeoutError::Disconnected) => Err(TimeoutError::TaskFailed("disconnected".into())),
    }
}

fn slow(delay_ms: u64, val: i32) -> Result<i32, String> {
    thread::sleep(Duration::from_millis(delay_ms)); Ok(val)
}

fn main() {
    println!("Fast: {:?}", with_timeout(Duration::from_millis(100), || slow(20, 42)));
    println!("Slow: {:?}", with_timeout(Duration::from_millis(30), || slow(200, 0)));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn succeeds() { assert_eq!(with_timeout(Duration::from_millis(200), || slow(10,42)).unwrap(), 42); }
    #[test] fn times_out() { assert!(matches!(with_timeout(Duration::from_millis(10), || slow(500,0)), Err(TimeoutError::Elapsed))); }
}
(* OCaml: timeout via channel with deadline *)

exception Timeout

let with_timeout secs f =
  let ch = Event.new_channel () in
  ignore (Thread.create (fun () -> Event.sync (Event.send ch (f ()))) ());
  match Event.sync (Event.choose [
    Event.wrap (Event.receive ch) (fun v -> Some v);
    Event.wrap (Event.timeout (int_of_float (secs *. 1e9))) (fun () -> None);
  ]) with
  | Some v -> v
  | None -> raise Timeout

let () =
  (try Printf.printf "Fast: %d\n" (with_timeout 1.0 (fun () -> Thread.delay 0.01; 42))
   with Timeout -> print_endline "timed out");
  (try ignore (with_timeout 0.01 (fun () -> Thread.delay 1.0; 0))
   with Timeout -> print_endline "correctly timed out")