openlang

`result.map_error` on every line of a use chain

use with result.try is my favourite bit of Gleam and I'm starting to resent it a little. Every step calls a different module, every module has its own error type, so every line ends up wearing a result.map_error.

pub fn load(id: Int) -> Result(Profile, AppError) {
  use row <- result.try(db.fetch(id) |> result.map_error(DbError))
  use raw <- result.try(decode(row) |> result.map_error(DecodeError))
  use profile <- result.try(validate(raw) |> result.map_error(Invalid))
  Ok(profile)
}

Passing constructors as functions keeps it short, sure. But half of each line is now plumbing, and the interesting part, what the step actually does, is squeezed in the middle.

One giant error type that every module imports is not the answer, that's worse. How do you all keep these chains readable? Helpers, a different shape, or just living with it?

The conversation continues 1

Follow the reasoning
  1. Permalink

    Tried one helper per error source, fn db(r) { result.map_error(r, DbError) }. Shorter and worse, db(db.fetch(id)) reads like a stutter, so I went back to inline and put each pipe on its own line.

Other sides of the same question

  1. `except ValueError` stopped catching after the move to TaskGroup

    Moved a small scoring script from asyncio.gather to asyncio.TaskGroup, and the fallback path stopped working. Before the chan…

    Replies 5python
  2. `String::from_utf8` gives the bytes back when it fails

    A consuming API has to decide what happens to the value when it fails. String::from_utf8 takes the Vec<u8> by value, and on f…

    Replies 0rust
  3. Where does the cursor stay after an incomplete record?

    Result tells the caller of a streaming parser whether a record came out. It says nothing about whether the parser moved a cur…