giusdp

giusdp

Hi all,

I was wondering on the best way to have a function that can return multiple errors. For example, I have something like this:

  @spec run_some_work(atom(), Params.t()) ::
          {:ok, Result.t()}
          | {:error, :bad_params}
          | {:error, :not_found}
          | {:error, :no_workers}
          | {:error, :worker_error}

The function can fail in different ways, and for each it has a different :error. in the @spec. I list them all so from the outside I can clearly see how to work with this function.

The problems that I’m having is that having functions with a big list of return types is cluttering my files, and that I have to update every spec if I add something new.

What’s your approach with functions that can give different errors? I was thinking about creating some sort of struct MyError, where the docs and specs would describe the possible errors it can contain, so in the function I would be just returning a single MyError struct.

Thanks in advance.

Showing Posts 1 to 7

cmo

cmo

You can use defexception to define the error, which you can pass around as a struct or raise.

You can also define the list of errors as a @type if you’re repeating them.

dimitarvp

dimitarvp

I’d follow Rust conventions and make the Result.t() type include both the :ok and :error variants, making sub-types out of them as necessary.

Depends how strict you want to be with the return values though, f.ex. if you have 8 variants for errors but you want to tell Dialyzer that function X can only return 3 of them then it could get tricky and verbose indeed, and in that case your current solution is the best.

giusdp

giusdp OP

Thanks for the advice. I like the idea of putting the possible errors in the Result struct itself, I was already slowly going in that direction. Perhaps it is a more functional way as well.

I will also look into the defexception approach, although initially I didn’t check it out because it seemed a more traditional OOP way to deal with errors.

dimitarvp

dimitarvp

I’m sure somebody will show up and tell me I’m wrong but I frankly don’t care about exceptions in Elixir. And the whole other throw, raise and rescue stuff. I ignore them on purpose.

I have only reached for them when it was made clear to me that a worker cannot ever crash (and thus be auto-restarted) at which point I just said “okay, fair” and bullet-proofed it.

In every single other case, so 99.9%, I’ll reach for everything else but not exceptions / throws / raises. If something is that brittle and can crash periodically – but recovers by itself little later – then I’ll just wrap it in a GenServer and communicate through messages with it (so as the caller doesn’t crash right away). But if that keeps crashing then it’s best for your app to go down because you have an unrecoverable error, in which case exceptions wouldn’t help at all anyway.


TL;DR: there’s a very good reasoning behind the “let it crash” philosophy, provided you have taken some precautions (which the OTP either gives you by default or makes extremely easy for you to set up e.g. amount of process restarts before giving up, timeouts etc).

cmo

cmo

You’re wrong! :wink:

An exception is a struct, so you can be more specific about the cause and house more details/context than a simple atom. You don’t have to raise it.

For example, I have an external system that can return either a windows or app error code, which we turn into one of two exceptions with the code and message. I can easily distinguish between them and handle the different error cases.

al2o3cr

al2o3cr

On a purely mechanical level, you can solve these problems with @type - anywhere you can write a basic type, you can instead write a custom one:

@type work_errors :: {:error, :bad_params} | {:error, :not_found} | {:error, :no_workers} | {:error, :worker_error}

@spec run_some_work(...) :: {:ok, Result.t()} | work_errors

That addresses both the “this takes up a lot of space” issue as well as “this is an updating headache” issue.

HOWEVER

A better question would be who’s handling those errors, and how. If they’re doing:

case run_some_work(...) do
  # etc
  {:error, _} ->
    yolo_try_it_again
end

then there’s not much value to maintaining detailed errors all the way up the stack. In that case, collapsing to a simpler error payload at a sensible boundary might make things clearer.

Finally, if the problem is likely to go away by just trying again (and if your data model tolerates it) you could consider “handling” errors like :no_workers by crashing.

giusdp

giusdp OP

Thanks everyone for the advice and the interesting discussion about the defexception, and the two schools of thoughts behind :smile:

In my case I don’t want to fall in the over-engineering trap so I’ll just use a @type to make the code less verbose and move on. If I find myself having to add more errors I will come back here and take a decision.

Probably I will do as @al2o3cr said and check out if I can move the error handling to a boundary, so I keep a lighter error type inside the core logic.

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
psy-q
I’m trying to set up Emacs with elixir-ls via lsp-mode and credo via Flycheck. This should mostly be preconfigured as Flycheck picks up c...
New

Other Trending Topics Top

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
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
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
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews