dogweather

dogweather

Rust is too low-level for most of my work, but I’m envious of the strong fp influence in its design. E.g., the verbose way to react to an error — and bubble it up — looks like this. The gen2() function also returns this Result Union/Either type:

fn caller() -> Result<String, String> {
    let r = gen2();
    match r {
        Ok(i) => Ok(i.to_string()),
        Err(e) => Err(e),
    }
}

To me, that looks a lot like the usual Elixir way of matching on {:ok, ...} and {:error, ...}. Rust, though, has this nice UX syntactic support with the ? operator. This code is equivalent to the above:

fn caller() -> Result<String, String> {
    let n = gen2()?;
    Ok(n.to_string())
}

(Source: https://betterprogramming.pub/rust-error-handling-84e7bd169e47)

Pretty damn nice, IMO. When I was doing a lot of Rust coding, error handling was a breezy, low-boilerplate experience — yet the code was as robust as ever. Invocations with ? like above can be chained together with the . operator.

I’m imagining syntactic support like this that’d work seamlessly with the existing :ok/:error convention, just like the Rust syntax works with Result.

Brainstorming and adapting the example from Writing Predictable Elixir Code with Reducers | AppSignal Blog … here’s the original code:

options = %{conn: conn}
 
with {:ok, payment} <- fetch_payment_information(params, options),
     {:ok, user}    <- fetch_user(conn),
     {:ok, address} <- fetch_address(%{user: user, params: params}, options),
     {:ok, order}   <- create_order(%{user: user, address: address, payment: payment}, options)
  do
  conn
  |> put_flash(:info, "Order completed!")
  |> redirect(to: Routes.order_path(conn, order))
else
  {:error, error_description} ->
    conn
    |> put_flash(:error, parse_error(error_description))
    |> render("checkout.html")
end

Let’s say we want to refactor this, extracting the with clause to a new function, but use a new => operator:

def process_payment(params, options) do
  payment <= fetch_payment_information(params, options),
  user    <= fetch_user(conn),
  address <= fetch_address(%{user: user, params: params}, options),

  order   <= create_order(%{user: user, address: address, payment: payment}, options)
end

Like Rust’s ?, this <= would automatically “unwrap” the return values, directly giving access to the :ok result. But if any of these returns :error it’ll automatically return the first found :error result. In a nutshell, it’d de-sugar to a with/else-construct.

The refactored caller could be standard un-sugared Elixir since it’s top-level and wants to handle the error, not bubble it up. But now it’s more readable:

options = %{conn: conn}
 
case process_payment(params, options) do
  {:ok, order} do
    conn
    |> put_flash(:info, "Order completed!")
    |> redirect(to: Routes.order_path(conn, order))
  {:error, error_description} ->
    conn
    |> put_flash(:error, parse_error(error_description))
    |> render("checkout.html")
end

This has been vetted and used in Rust for years. So even though it hides some info and does some things automatically, I think the Rust experience has validated it as a positive addition to the language.

Along with <= I was thinking about an unwrapping version of the pipe |>: This new version would transparently handle functions that return the :ok/:error tuple, enabling them to be used in pipelines with functions that don’t. IMO this second syntax could make code even cleaner, e.g. if the new pipe is !>:

username
|> to_lower
!> get_user_from_database
|> format_address

Showing Posts 1 to 8

cmo

cmo

While with is a little more verbose, it has the benefit of being more explicit and more flexible, in that you can match on anything - not just an ok tuple.

F# has the result computation expression which I loved because I found working with the result monad to be a total PITA without it.

mpope

mpope

One thing that is often missed at a glance with with is the inability to specify ordered fallback handling. For example, say there is a response that is {:error, "arbitrary_error_string"} that is returned by two functions my_fun_1 and my_fun_2. However, I as the application developer want two different rollback operations based on if my_fun_1 fails or if my_fun_2 fails, and they’re part of two separate 3rd party libraries, so I have no control over what they return (a scenario I’ve ran into). They both happen to return an error tuple that lacks uniqueness.

What I see lacking from this feature is something similar: a generic ordered error handler. However, it seems to have a much higher potential to fulfill that void if error handler functions were added:

payment <= fetch_payment_information(params, options), &handle_fetch_payment_error/1
user    <= fetch_user(conn), &handle_fetch_user_error/1
address <= fetch_address(%{user: user, params: params}, options), &handle_fetch_address_error/1
order   <= create_order(%{user: user, address: address, payment: payment}, options), &handle_create_order_error/1

But in theory this would just rely on pattern matching in the head of the handle_x_error functions, which is already a well defined feature of the language:

user = fetch_user(conn) |> handle_fetch_user_error()

def handle_fetch_user_error({:error, db_error_struct}) do
...
end
def handle_fetch_user_error({:ok, user}), do: user
al2o3cr

al2o3cr

I don’t follow having “no control” - what about wrapping them in private functions?

Two thoughts:

  • “cleaner” is subjective, IMO the ! vs | visual distinction is very small
  • you can already write functions like this if you want with plain |>, but it means that downstream functions all grow a {:error, _} pass-through case
ityonemo

ityonemo

Elixir is sort of categorically different from rust in that failure is more than just a option, it’s a strategy. Having error tuples exposed and handled is a signal that you can tolerate an error in this location and you’re willing to deal with it, versus triggering a crash and giving up. Hiding that, IMO, loses a signal that qualitatively tells you something about the code that is flowing through your system.

opsb

opsb

There’s a couple of libraries that provide macros towards this goal. Off the top of my head OK Elixir – ok v0.2.0 might be be appealing?

mpope

mpope

True, wrapping them in private functions would have been differentiable.

krasenyp

krasenyp

Can’t you use something like the following to discriminate between error tuples of my_fun_1 and my_fun_2?

with {:mf1, {:ok, _}} <- my_fun_1(...),
     {:mf2, {:ok, _}} <- my_fun_2(...) do
  ...whatever...
else
  {:mf1, {:error, _}} -> rollback_my_fun_1(...)
  {:mf2, {:error, _}} -> rollback_my_fun_2(...)
end
mpope

mpope

These are both valid workarounds, but they still work-around a lack of syntax for ordered error handling in with. This could be out of scope for with, having to create a bunch of transient terms to shoe-horn this into a language feature could be just abusing the feature. The original proposal has the potential to make ordered error handling cleaner.

— All posts loaded —

Where Next? Top

Trending in Discussions Top

cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
axelson
Hi there! :wave: @frigidcode and I (but mostly him) have been running an Elixir Book club, we’re almost done with Designing Elixir Syste...
New
achempion
I’ve been using Emacs as my main code editor for more than a two years. It’s a custom build version although I’ve tried doom emacs and sp...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
georgeguimaraes
Just published claude-code-elixir, a plugin marketplace for Claude Code with Elixir support. These are the plugins I’ve been using for my...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews