PragTob

PragTob

Library API Design error returns: nil vs error tuple vs exception

:wave:

I’m currently extracting the statistics calculation part from benchee and stumbled upon how to present error conditions. In the concrete example statistics of an empty list [] don’t make much sense. There’s no average, no total, no nothing.

I see three possible ways to represent this error condition right now:

  • return nil - feels kind of right as in this specific case an empty list just doesn’t have an average, a total or what not. So it’s nothing. Feels kind of wrong because some of these also return nil values normally (there doesn’t have to be a mode). Also nil tends to leak and be a bit hard to debug.
  • return {:error, reason} - this seems to be the most conventional wisdom in elixir looking around. Mostly it’s paired with an {:ok, value} return value which for my concrete use case I’m not the biggest fan of. It’s easy to match against etc. and should one decide to want to introduce an exception raising API it’d be easy to implement !-bang methods that raise instead of the tuple. What I also like about this, is that it’s easy to represent in the type spec.
  • raise a (custom) exception - this is mostly discouraged in elixir, the main argument being that it should only be used if it would leave the application in an undefined state. Here’s [a good blog post] (https://michal.muskala.eu/post/error-handling-in-elixir-libraries/) by @michalmuskala going over error tuples vs. exceptions. So why am I even considering this? I find it curious that the default behavior of Enum.max/2 and Enum.min/2 is to raise an exception (my min/max functions basically just wrap these :angel:) . However, Enum.sum/1 returns 0. So at least part of the elixir stdlib does this (to an extent, you can pass your own empty element function).

I think it’s important to consider what kind of usage each pattern promotes for the users of a library:

  • nil - get some error that they had some nil value somewhere where they were expecting something else having to debug it back to the library call and ultimately the bad value they provided. Not great.
  • {:error, reason} - pattern match on calls either in case or directly assertive through {:ok, result} = Lib.call(input) which I think is ok. However, if users didn’t read the docs (bad!) tracing back where that tuple originated instead of a real value came from might still be a bit of a bother.
  • exception - never ever pass me that value i.e. the value should be caught by users beforehand. If they don’t the stack trace points them right where they need to look and fix the input (hopefully with a very good error message). I kind of like this for my specific case as handing an empty list to a statistics function seems like something that mostly shouldn’t be done and easy to catch on the user side. However, it’s not an “application critical state”, definitely recoverable and might be considered a valid input.

Right now I use value return but an {:error, reason} error return type - which I realize is sub optimal as it’s hard to match without an {:ok, value}. I kind of like just raising exceptions for my specific case but it feels wrong to do so. After having written all this it feels like implementing both (with !-bang methods) seems like the safest choice without too much extra work.

What’s your take? What do you think about the different options in general? What do you think about the different options in the concrete statistics calculation use case?

Most Liked

michalmuskala

michalmuskala

I agree with @sasajuric - passing in an empty list to a function that doesn’t make sense on empty lists is most probably a programmer error and should just fail. I’d say that the case here is almost the definition of the ArgumentError exception.

11
Post #5
sasajuric

sasajuric

Author of Elixir In Action

In this case I’d raise an exception for two reasons:

  • according to your description, an empty list is a non-domain input (e.g. because avg([]) is undefined)
  • library users can easily handle empty list anywhere up in the call stack

In addition, I’d communicate this through typespec, by using [...] or [element_type, ...].

PragTob

PragTob

Thank you everyone for your input! Before the post my hunch was to go with an exception but it somehow also felt wrong. I’ll do that now and raise an ArgumentError as suggested by many of you! :+1:

Thanks for being such a helpful bunch! :tada:

@xlphs I’d rather avoid that the users of my lib see errors involved with what the library does but rather what they did “wrong” so I think ArgumentError is better :slight_smile:

@Qqwy It’s hard to say how often they’ll do it as a library can be used in all sorts of ways imo :slight_smile: I think they should never do it/if it can be empty catch it before ideally though. I like your thoughts on the ways to generate “useless values” - thank you!

@sasajuric yup thank you, type spec will definitely be [number, ...], I remember the dialyzer on benchee complaining when I did that though but that just means I need to tune something :grin:

sashman

sashman

As a library user my vote would be a tuple return in general.

dimitarvp

dimitarvp

It doesn’t make sense to return anything in your case. I’d just do raise(ArgumentError, "cannot calculate average of an empty list").

Where Next?

Popular in Discussions Top

artimath
I think I’ve tried 5 different graph database libraries in the last two days and not a single one has been able to connect to a remote/lo...
New
lichuan
Hi, I’ve created a new repository on GitHub to compare the fairness and real-time performance of schedulers in Erlang and Go. Feel free t...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
lud
Hello, I just extracted the boilerplate management code that I used to work with in previous years: It is yet another generic input d...
New
yordisprieto
I am trying to figure out a good architecture and implementation for authZ. Recently, I discover GitHub - open-policy-agent/opa: Open Po...
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
michallepicki
Hello! To save some time when switching projects or development machines, I glued together an asdf plugin that uses Erlang precompiled b...
New
karlosmid
The |> operator appears in many languages, mostly in the functional world. F# has essentially the exact same operator, as does OCaml. ...
New
kusokuzeshiki
I tried to generate pages with claude code and fluxon ui. I ask to claude code to generate real world page samples, the results are thes...
New
gushonorato
Hey everyone, I’ve been working with Elixir for over 5 years and I’m a big enthusiast of the language. However, when starting new projec...
New

Other popular topics Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New

We're in Beta

About us Mission Statement