Learn Rust Series (#46) - Result Combinators: map, map_err, and_then, ok_or

Published on HivePostify by @scipio · Thu Sep 03 2026

Learn Rust Series (#46) - Result Combinators: map, maperr, andthen, okor

What will I learn - You will learn how to transform a Result without writing a match at every step; - how map and maperr change the Ok and Err sides independently; - how andthen chains fallible operations, and how orelse recovers from errors; - the unwrapor, unwraporelse, and unwrapordefault family for supplying fallbacks; - how ok, okor, and okorelse bridge between Result and Option, and how to build clean pipelines.

Requirements - A working modern computer running macOS, Windows or Ubuntu; - An installed Rust toolchain (via rustup, from rustup.rs); - The previous forty-five episodes, especially error handling (episode 6) and closures (episode 11); - The ambition to learn systems programming from the ground up.

Difficulty - Intermediate

Curriculum (of the Learn Rust Series): - [Learn Rust Series (#1) - Introduction to Rust](https://hive.blog/hive-196387/@scipio/learn-rust-series-1-introduction-to-rust) - [Learn Rust Series (#2) - Variables, Types, Functions](https://hive.blog/hive-196387/@scipio/learn-rust-series-2-variables-types-functions) - [Learn Rust Series (#3) - Ownership & Borrowing](https://hive.blog/hive-196387/@scipio/learn-rust-series-3-ownership-borrowing) - [Learn Rust Series (#4) - Control Flow & Pattern Matching](https://hive.blog/hive-196387/@scipio/learn-rust-series-4-control-flow-pattern-matching) - [Learn Rust Series (#5) - Structs & Enums](https://hive.blog/hive-196387/@scipio/learn-rust-series-5-structs-enums) - [Learn Rust Series (#6) - Error Handling](https://hive.blog/hive-196387/@scipio/learn-rust-series-6-error-handling) - [Learn Rust Series (#7) - Collections](https://hive.blog/hive-196387/@scipio/learn-rust-series-7-collections) - [Learn Rust Series (#8) - Traits & Generics](https://hive.blog/hive-196387/@scipio/learn-rust-series-8-traits-generics) - [Learn Rust Series (#9) - Modules & Crates](https://hive.blog/hive-196387/@scipio/learn-rust-series-9-modules-crates) - [Learn Rust Series (#10) - Lifetimes](https://hive.blog/hive-196387/@scipio/learn-rust-series-10-lifetimes) - [Learn Rust Series (#11) - Closures & the Iterator Trait](https://hive.blog/hive-196387/@scipio/learn-rust-series-11-closures-the-iterator-trait) - [Learn Rust Series (#12) - Smart Pointers: Box, Rc & RefCell](https://hive.blog/hive-196387/@scipio/learn-rust-series-12-smart-pointers-box-rc-refcell) - [Learn Rust Series (#13) - Concurrency: Threads, Channels, Arc & Mutex](https://hive.blog/hive-196387/@scipio/learn-rust-series-13-concurrency-threads-channels-arc-mutex) - [Learn Rust Series (#14) - Mini Project: A Command-Line To-Do App](https://hive.blog/hive-196387/@scipio/learn-rust-series-14-mini-project-a-command-line-to-do-app) - [Learn Rust Series (#15) - Trait Objects & Dynamic Dispatch](https://hive.blog/hive-196387/@scipio/learn-rust-series-15-trait-objects-dynamic-dispatch) - [Learn Rust Series (#16) - Static vs Dynamic Dispatch](https://hive.blog/hive-196387/@scipio/learn-rust-series-16-static-vs-dynamic-dispatch) - [Learn Rust Series (#17) - Associated Types vs Generic Parameters](https://hive.blog/hive-196387/@scipio/learn-rust-series-17-associated-types-vs-generic-parameters) - [Learn Rust Series (#18) - Operator Overloading with std::ops](https://hive.blog/hive-196387/@scipio/learn-rust-series-18-operator-overloading-with-stdops) - [Learn Rust Series (#19) - Deref, DerefMut & Deref Coercion](https://hive.blog/hive-196387/@scipio/learn-rust-series-19-deref-derefmut-deref-coercion) - [Learn Rust Series (#20) - Drop & Deterministic Destruction (RAII)](https://hive.blog/hive-196387/@scipio/learn-rust-series-20-drop-deterministic-destruction-raii) - [Learn Rust Series (#21) - From, Into, TryFrom & Idiomatic Conversions](https://hive.blog/hive-196387/@scipio/learn-rust-series-21-from-into-tryfrom-idiomatic-conversions) - [Learn Rust Series (#22) - Deriving Common Traits](https://hive.blog/hive-196387/@scipio/learn-rust-series-22-deriving-common-traits) - [Learn Rust Series (#23) - The Orphan Rule & Trait Coherence](https://hive.blog/hive-196387/@scipio/learn-rust-series-23-the-orphan-rule-trait-coherence) - [Learn Rust Series (#24) - Blanket Implementations & the Newtype Pattern](https://hive.blog/hive-196387/@scipio/learn-rust-series-24-blanket-implementations-the-newtype-pattern) - [Learn Rust Series (#25) - Marker Traits: Sized, Send, Sync & Copy](https://hive.blog/hive-196387/@scipio/learn-rust-series-25-marker-traits-sized-send-sync-copy) - [Learn Rust Series (#26) - Const Generics: Types That Depend on Values](https://hive.blog/hive-196387/@scipio/learn-rust-series-26-const-generics-types-that-depend-on-values) - [Learn Rust Series (#27) - Generic Associated Types & Lending Iterators](https://hive.blog/hive-196387/@scipio/learn-rust-series-27-generic-associated-types-lending-iterators) - [Learn Rust Series (#28) - Sealed Traits & Designing Stable APIs](https://hive.blog/hive-196387/@scipio/learn-rust-series-28-sealed-traits-designing-stable-apis) - [Learn Rust Series (#29) - Typestate Programming: State Machines in the Type System](https://hive.blog/hive-196387/@scipio/learn-rust-series-29-typestate-programming-state-machines-in-the-type-system) - [Learn Rust Series (#30) - Mini Project: A Generic Units-of-Measure Library](https://hive.blog/hive-196387/@scipio/learn-rust-series-30-mini-project-a-generic-units-of-measure-library) - [Learn Rust Series (#31) - Move Semantics Deep Dive](https://hive.blog/hive-196387/@scipio/learn-rust-series-31-move-semantics-deep-dive) - [Learn Rust Series (#32) - Interior Mutability: Cell & RefCell](https://hive.blog/hive-196387/@scipio/learn-rust-series-32-interior-mutability-cell-refcell) - [Learn Rust Series (#33) - Rc Internals: Reference Counting & Shared Ownership](https://hive.blog/hive-196387/@scipio/learn-rust-series-33-rc-internals-reference-counting-shared-ownership) - [Learn Rust Series (#34) - Arc: Thread-Safe Reference Counting & Its Cost](https://hive.blog/hive-196387/@scipio/learn-rust-series-34-arc-thread-safe-reference-counting-its-cost) - [Learn Rust Series (#35) - Weak References & Breaking Reference Cycles](https://hive.blog/hive-196387/@scipio/learn-rust-series-35-weak-references-breaking-reference-cycles) - [Learn Rust Series (#36) - Cow: Clone-on-Write for Borrow-or-Own APIs](https://hive.blog/hive-196387/@scipio/learn-rust-series-36-cow-clone-on-write-for-borrow-or-own-apis) - [Learn Rust Series (#37) - Pin & Self-Referential Structs](https://hive.blog/hive-196387/@scipio/learn-rust-series-37-pin-self-referential-structs) - [Learn Rust Series (#38) - PhantomData, Zero-Sized Types & Marker Lifetimes](https://hive.blog/hive-196387/@scipio/learn-rust-series-38-phantomdata-zero-sized-types-marker-lifetimes) - [Learn Rust Series (#39) - Variance: Covariance, Contravariance & Why It Matters](https://hive.blog/hive-196387/@scipio/learn-rust-series-39-variance-covariance-contravariance-why-it-matters) - [Learn Rust Series (#40) - Arena & Bump Allocation Patterns](https://hive.blog/hive-196387/@scipio/learn-rust-series-40-arena-bump-allocation-patterns) - [Learn Rust Series (#41) - Building Your Own Smart Pointer](https://hive.blog/hive-196387/@scipio/learn-rust-series-41-building-your-own-smart-pointer) - [Learn Rust Series (#42) - Drop Order, the Drop Check & Leak Safety](https://hive.blog/hive-196387/@scipio/learn-rust-series-42-drop-order-the-drop-check-leak-safety) - [Learn Rust Series (#43) - std::mem: swap, replace, take & forget](https://hive.blog/hive-196387/@scipio/learn-rust-series-43-stdmem-swap-replace-take-forget) - [Learn Rust Series (#44) - Higher-Ranked Trait Bounds & Lifetime Elision](https://hive.blog/hive-196387/@scipio/learn-rust-series-44-higher-ranked-trait-bounds-lifetime-elision) - [Learn Rust Series (#45) - Mini Project: A Doubly-Linked List, Safe then Unsafe](https://hive.blog/hive-196387/@scipio/learn-rust-series-45-mini-project-a-doubly-linked-list-safe-then-unsafe) - [Learn Rust Series (#46) - Result Combinators: map, maperr, andthen, okor](https://hive.blog/hive-196387/@scipio/learn-rust-series-46-result-combinators-map-maperr-andthen-okor) (this post)

Learn Rust Series (#46) - Result Combinators: map, maperr, andthen, okor

Welcome to Phase 4, which is all about making code robust and shippable, and it begins where every real program spends a serious slice of its life: handling errors. We are done, for now, with the deep memory-model spelunking of Phase 3 - the Rcs and the Weaks and the raw pointers - and we are going to do something rather more relaxing, as I promised at the end of last episode. You already met Result and the ? operator way back in episode 6. What you did not get then were its combinators: the methods that transform, chain, and unwrap a Result without a match block at every corner. Written well, error-handling code stops looking like a staircase of nested matches and starts reading like a clean pipeline of operations, top to bottom ;-)

Having said that, let me be clear about what this episode is and is not. It is not "here is a wall of forty methods, memorise them". It is a small, carefully chosen toolkit - maybe eight methods - that between them cover almost every fallible flow you will ever write. Learn these eight and the match blocks largely melt away. But before we get to any of that, we owe episode 45 its homework, because last time I left three exercises on the doubly-linked list, and skipping the solutions would be cheating you.

Solutions to Episode 45 Exercises

Episode 45 was the doubly-linked list, built safely with Rc/RefCell/Weak and then unsafely with raw pointers. There were three exercises, and here is full, runnable code for each - the whole program, not a fragment, so you can paste and run it.

Exercise 1 asked for a pushback on the safe list, using the tail weak pointer. The trick is to upgrade the stored Weak into a real Rc so you can attach the new node behind it: rust use std::rc::{Rc, Weak}; use std::cell::RefCell;

type Link = Option >>;

struct Node { value: i32, next: Link, prev: Option >> } struct DoublyLinkedList { head: Link, tail: Option >> }

impl DoublyLinkedList { fn new() -> DoublyLinkedList { DoublyLinkedList { head: None, tail: None } }

fn pushback(&mut self, value: i32) { let node = Rc::new(RefCell::new(Node { value, next: None, prev: None })); match self.tail.take().andthen(|w| w.upgrade()) { Some(oldtail) => { node.borrowmut().prev = Some(Rc::downgrade(&oldtail)); oldtail.borrowmut().next = Some(Rc::clone(&node)); } None => self.head = Some(Rc::clone(&node)), } self.tail = Some(Rc::downgrade(&node)); }

fn tovec(&self) -> Vec { let mut out = Vec::new(); let mut cursor = self.head.clone(); while let Some(node) = cursor { out.push(node.borrow().value); cursor = node.borrow().next.clone(); } out } }

fn main() { let mut list = DoublyLinkedList::new(); list.pushback(1); list.pushback(2); println!("{:?}", list.tovec()); // [1, 2] }

The key insight is self.tail.take().andthen(|w| w.upgrade()): take pulls the old tail's Weak out, and upgrade turns it into an Rc only if that node is still alive. If the list was empty there is no tail to upgrade, so the new node becomes the head instead. Either way, the new node ends up as the tail, held weakly, so no cycle forms.

Exercise 2 wanted a popfront on the unsafe RawList, unlinking the head, reclaiming it, and fixing up the new head's prev: rust use std::ptr::NonNull;

struct Node { value: i32, next: Option >, prev: Option > } struct RawList { head: Option >, tail: Option >, len: usize }

impl RawList { fn new() -> RawList { RawList { head: None, tail: None, len: 0 } }

fn pushfront(&mut self, value: i32) { let node = NonNull::new(Box::intoraw(Box::new(Node { value, next: self.head, prev: None }))).unwrap(); match self.head { Some(mut old) => unsafe { old.asmut().prev = Some(node) }, None => self.tail = Some(node), } self.head = Some(node); self.len += 1; }

fn popfront(&mut self) -> Option { self.head.map(|node| { // SAFETY: node came from Box::intoraw and is still live and owned by us. let boxed = unsafe { Box::fromraw(node.asptr()) }; self.head = boxed.next; match self.head { Some(mut newhead) => unsafe { newhead.asmut().prev = None }, None => self.tail = None, } self.len -= 1; boxed.value }) } }

impl Drop for RawList { fn drop(&mut self) { let mut cursor = self.head; while let Some(node) = cursor { // SAFETY: reclaim each leaked Box exactly once, in order. unsafe { let boxed = Box::fromraw(node.asptr()); cursor = boxed.next; } } } }

fn main() { let mut list = RawList::new(); list.pushfront(2); list.pushfront(1); println!("{:?}", list.popfront()); // Some(1) println!("{:?}", list.popfront()); // Some(2) println!("{:?} len {}", list.popfront(), list.len); // None len 0 }

Box::fromraw is the star: it reclaims ownership of the leaked head allocation, so when boxed drops at the end of the closure the memory is freed exactly once. When the list becomes empty we clear the tail too, otherwise it would dangle. Notice the code returns cleanly even after everything is popped - the surviving Drop still walks a now-empty list and frees nothing, which is correct.

Exercise 3 was a tovecreversed on the safe list, walking backward from tail along the prev links: rust use std::rc::{Rc, Weak}; use std::cell::RefCell;

type Link = Option >>;

struct Node { value: i32, next: Link, prev: Option >> } struct DoublyLinkedList { head: Link, tail: Option >> }

impl DoublyLinkedList { fn new() -> DoublyLinkedList { DoublyLinkedList { head: None, tail: None } }

fn pushfront(&mut self, value: i32) { let node = Rc::new(RefCell::new(Node { value, next: self.head.take(), prev: None })); match &node.borrow().next { Some(next) => next.borrowmut().prev = Some(Rc::downgrade(&node)), None => self.tail = Some(Rc::downgrade(&node)), } self.head = Some(node); }

fn tovec(&self) -> Vec { let mut out = Vec::new(); let mut cursor = self.head.clone(); while let Some(node) = cursor { out.push(node.borrow().value); cursor = node.borrow().next.clone(); } out }

fn tovecreversed(&self) -> Vec { let mut out = Vec::new(); let mut cursor = self.tail.clone().andthen(|w| w.upgrade()); while let Some(node) = cursor { out.push(node.borrow().value); cursor = node.borrow().prev.clone().andthen(|w| w.upgrade()); } out } }

fn main() { let mut list = DoublyLinkedList::new(); list.pushfront(3); list.pushfront(2); list.pushfront(1); println!("{:?}", list.tovec()); // [1, 2, 3] println!("{:?}", list.tovecreversed()); // [3, 2, 1] }

Each backward step is prev.clone().andthen(|w| w.upgrade()) - because prev is a Weak, we must upgrade it to touch the node, and andthen gracefully stops the walk if any link has died. And there it is: andthen and upgrade already showing up in last week's homework, which is the perfect segue, because andthen is one of the stars of this episode. Right, homework cleared. Now, Result combinators.

map and maperr: transform each side

A Result has two sides, and the first thing you want to do is transform one of them without disturbing the other. map applies a function to the Ok value and leaves an Err completely untouched; maperr does the mirror image, transforming the error and leaving Ok alone: rust fn main() { let ok: Result = Ok(5); let doubled = ok.map(|n| n 2); // Ok(10) println!("{doubled:?}");

let err: Result = Err(String::from("bad input")); let wrapped = err.maperr(|e| format!("error: {e}")); // Err("error: bad input") println!("{wrapped:?}"); }

Think of it this way: map is "if this succeeded, keep going with the success reshaped", and maperr is "if this failed, translate the failure into my vocabulary". That second one is more important than it looks. A huge amount of real error handling is translation - a low-level std::io::Error or a ParseIntError bubbling up from some library, which you want to convert into your program's error type before it goes any further. That is exactly the job maperr does, and it is the very same conversion the ? operator performs automatically via the From trait we studied in episode 21. When ? does not have a From impl to lean on, a manual maperr is how you bridge the gap.

The mental model that helps here: a Result is a box with two compartments, and map/maperr reach into exactly one compartment, apply your closure, and put the box back together unchanged in every other respect. Neither method can turn an Ok into an Err or vice versa - they preserve the variant. For that you need the next tool.

andthen: chain operations that can themselves fail

map is for a function that always succeeds - double a number, format a string. But very often the next step in a computation can itself fail, and returns its own Result. If you reach for map there, you get a Result , E> - a box inside a box, which is a nuisance. andthen is the answer: it chains a fallible step and flattens the result, so a chain of fallible steps stays flat: rust fn parsepositive(s: &str) -> Result { s.parse:: () .maperr(|| format!("'{s}' is not a number")) .andthen(|n| { if n > 0 { Ok(n) } else { Err(format!("{n} is not positive")) } }) }

fn main() { println!("{:?}", parsepositive("42")); // Ok(42) println!("{:?}", parsepositive("-1")); // Err("-1 is not positive") println!("{:?}", parsepositive("abc")); // Err("'abc' is not a number") }

Read parsepositive as a little assembly line. First we try to parse the string; if that fails we translate the parse error into a friendly message with maperr. Then andthen runs a second fallible check - is the number positive? - which returns its own Ok/Err. The lovely part is the short-circuiting: if the parse fails, the andthen closure never runs at all, and the first error wins. Each step only executes if every step before it succeeded. If you know Haskell or Scala, andthen is the "flat map" or "bind" of Result; if you do not, just remember it as "chain another thing that might fail, and keep it flat".

The distinction between map and andthen is the single most important idea in this episode, so let me put it bluntly. Use map when your closure returns a plain value T. Use andthen when your closure returns a Result . Get those two straight and everything else falls into place.

Fallbacks: unwrapor and its family

Sometimes you do not want to propagate an error at all - you want to leave the Result world right here, with a sensible default, and carry on with a plain value. The unwrapor family does exactly that, and each member trades off differently: rust fn main() { let ok: Result = Ok(10); let err: Result = Err(String::from("oops"));

println!("{}", ok.unwrapor(0)); // 10 println!("{}", err.clone().unwrapor(0)); // 0 println!("{}", err.clone().unwraporelse(|e| e.len() as i32)); // 4 let missing: Result = Err(String::from("x")); println!("{}", missing.unwrapordefault()); // 0 }

Three flavours, three use-cases. unwrapor(value) hands back a fixed fallback you supply up front - simplest, but the fallback is always evaluated even when the Result is Ok, so keep it cheap. unwraporelse(closure) computes the fallback lazily, only when there actually is an error, and it even hands you the error so you can decide the fallback based on what went wrong - use this when building the default costs something (an allocation, a lookup). And unwrapordefault() uses the type's Default impl, which for i32 is 0, for String is "", for Vec is empty - handy when "the zero value is fine" is genuinely true.

All three are dramatically safer than the blunt unwrap(), which panics the whole thread on an Err, and its cousin expect(), which panics with a message of your choosing. Save unwrap and expect for the genuinely-cannot-fail cases (a hard-coded regex that you know compiles, a value you just inserted) - and even then, expect with a clear message is kinder to whoever debugs the panic at 2 a.m. In everyday code, prefer the unwrapor family or, better yet, propagate with ? and let the caller decide.

Tags: #stem#stemsocial#steemstem#rust#programming

View full post on HivePostify →

Join HivePostify — Pakistan's First Web3 Platform →