jswny

jswny

Hi all! I’m fairly new to Elixir. I’ve read a book or two, but am just getting into using the language in practice. I recently needed to write a simple function to update a key in a JSON file. However, I found myself running into a lot of repetitive Go-esque error checking. I’m new to functional programming and I was wondering if there is a more idomatic way to handle this type of situation, or just a way to improve it. I’m not a fan of this style as it is because it’s a huge block of ever-indenting lines like old JavaScript. Here is a slimmed down example I came up with:

defmodule Test do
  @doc """
  Inserts the given JSON into the `data` field of `file`.
  """
  def insert(file, data) do
    # Read the contents of the file
    case File.read(file) do
      {:ok, content} -> 

        # Decode the file to JSON
        case Poison.decode(content) do
          {:ok, decoded_content} ->

            # Encode the given data to JSON
            case Poison.encode(data) do
              {:ok, encoded_data} ->

                # Prepare the updated data for insertion
                final_data = Map.update(decoded_content, "data", encoded_data, fn _ -> encoded_data end)

                  # Encode the updated data
                  case Poison.encode(final_data) do
                    {:ok, encoded_final_data} ->

                      # Write the updated data to the file
                      case File.write(file, encoded_final_data) do
                        :ok -> IO.puts "Successfully updated file!"
                        {:error, reason} -> IO.puts "Could not write to file because #{reason}!"
                      end
                    {:error, {:invalid, reason}} -> IO.puts "Could not encode updated JSON because #{reason}!"
                  end

              {:error, {:invalid, reason}} -> IO.puts "Could not encode given JSON because #{reason}!"
            end

          {:error, :invalid} -> IO.puts "Could not parse file to JSON!"
          {:error, :invalid, reason} -> IO.puts "Could not parse file to JSON because #{reason}!"
        end

      {:error, reason} -> IO.puts "Could not read file because #{reason}!"
    end
  end
end

Keep in mind, I just wrote this as a quick example. It’s not my actual code nor is it perfect code. I’m just trying to demonstrate the error handling pattern I’m Talking about. Thanks in advance!

Showing Posts 1 to 10

sergio

sergio

I got this tip courtesy of the Pragmatic Studio Elixir course.

Instead of nesting case statements, use function pattern matching to handle conditional branches.

File.read(file)
|> handle_file

def handle_file({:ok, content}) do
  Poison.decode(content) 
  |> handle_encode
end

def handle_file({:error, reason}) do
  # Something someting
end

And so on.

svarlet

svarlet

I’m currently playing with the Exceptional library:

In short, it explains that error tuples are annoying for error handling. They break pipelines, they have variable shapes which makes them hard to pattern-match ({:error, :reason} or sometimes {:error, {:foo, :bar}} or whatever). On the other hand, pattern matching on maps gives you total flexibility in terms of what your error value can contain. So instead of returning error tuples, what if you returned error maps? and Exceptions in elixir are just structs which are maps.

Add a new operator f ~> g which behaves like f |> g except that if the input is an exception then it will bypass g and return the exception. Yes we are not raising exceptions, but returning exceptions like any other values.

Check the post on medium, it’s well explained and that is working well for me now.

dom

dom

You can use the with/else syntax in some cases, pipelining through several functions that match on their input and return it unmodified if it’s an error is also an option.

More generally though, it’s better not to handle an error case explicitly unless it’s expected to happen and you need to send a helpful message back to the caller/user. A supervisor crash log is a lot more useful for debugging than a vague log message like “could not read file” etc. See Erlang and code style: Musings on mostly defensive programming styles.

yurko

yurko

Using with helps with that kind of logic, basically you describe the happy path and can be as generic or specific as you want when handling errors (s. else example).

peerreynders

peerreynders

All the other advice is good - but at the most basic level keep your functions small, tiny even - e.g.:

defmodule Test do

  defp reportOnFileWrite(:ok),
    do: IO.puts "Successfully updated file!"
  defp reportOnFileWrite({:error, reason}),
    do: IO.puts "Could not write to file because #{reason}!"

  defp writeEncodedFinalData({:ok, encoded_final_data}, file),
    do: reportOnFileWrite (File.write file, encoded_final_data)
  defp writeEncodedFinalData({:error, {:invalid, reason}}, _),
    do: IO.puts "Could not encode updated JSON because #{reason}!"

  defp writeEncodedData({:ok, encoded_data}, file, decoded_content) do
    # Prepare the updated data for insertion
    final_data = Map.update(decoded_content, "data", encoded_data, fn _ -> encoded_data end)
    # Encode the updated data
    writeEncodedFinalData (Poison.encode final_data), file
  end
  defp writeEncodedData({:error, {:invalid, reason}}, _, _) do
    IO.puts "Could not encode given JSON because #{reason}!"
  end

  defp writeDecodedContent({:ok, decoded_content}, file, data),
    do: writeEncodedData (Poison.encode data), file, decoded_content
  defp writeDecodedContent({:error, :invalid}, _, _),
    do: IO.puts "Could not parse file to JSON!"
  defp writeDecodedContent({:error, :invalid, reason}, _, _),
    do: IO.puts "Could not parse file to JSON because #{reason}!"

  defp rewrite({:ok, content}, file, data),
    do: writeDecodedContent (Poison.decode content), file, data
  defp rewrite({:error, reason}, _, _),
    do: IO.puts "Could not read file because #{reason}!"

  @doc """
  Inserts the given JSON into the `data` field of `file`.
  """
  def insert(file, data) do
    # Read the contents of the file
    rewrite (File.read file), file, data
  end
end

This is essentially just a starting point for all the other pieces of advice you are getting. Once things are broken down in this manner it becomes much easier to factor things out.

Never forget that everything is an expression (forget about statements).

Crowdhailer

Crowdhailer

Creator of Raxx

I wrote about how i think about this problem a while ago. Handling Errors in Elixir, No one say Monad.

In summary I think with is useful but I prefer to sacrifice the flexibility of with for something that only handles {:ok, value} and {:error, reason} and built a library just for those cases. GitHub - CrowdHailer/OK: Elegant error/exception handling in Elixir, with result monads. · GitHub

Finally keeping function small is always good ideas. If you have 8 places that can throw an error ( no matter how neat the code) its probably too many

aseigo

aseigo

.. but they don’t have to. See GitHub - CrowdHailer/OK: Elegant error/exception handling in Elixir, with result monads. · GitHub for a way this could be done.

edit: aaaand now I see that @Crowdhailer posted that same link, too … if only I’d read the thread all the way. oops!

jswny

jswny OP

Thank you all for your informative responses! Because this functionality is only a small section of my app, I don’t want to use any libraries so I’ve taken the advice and refactored my code using functional pattern matching like handle_file_read({:ok, content}), etc. However, I must say the libraries provided in this thread do look enticing and I would certainly consider them if my app had a larger need for such error handling. I’m going to try to use with statements as well to improve my code as I haven’t used those before or in other languages that I’ve written in even. Thanks again everyone!

jswny

jswny OP

I’d like to respond to @dom’s suggestion separately. I’m very much considering dropping all of this error handling that I’m doing manually since all I am really accomplishing is printing out user-friendly error messages each step of the way so that the user does not have to decode them. Since I’m still raising exceptions and stopping execution each time I get something like an {:error, reason} response, it would make sense for me to take the optimistic route and let the functions naturally raise exceptions themselves if something is wrong. However, the end-user would not easily be able to decode these messages. It seems like a trade-off of code maintainability and conciseness for user-friendliness that I have to weigh.

peerreynders

peerreynders

In implementations with imperative languages I often encounter a certain unwillingness to acknowledge the complexity burden that detailed error handling imposes which typically devolves in rampaging Arrow(head)s (especially in JavaScript) and insufficient separation/segregation of “happy path” from “unhappy path” code. “Unhappy path” code deserves the same “management” consideration as the “happy path” code that the domain logic is primarily concerned with.

defmodule Test do

  @doc """
  Inserts the given JSON into the `data` field of `file`.
  """
  def insert(file, data) do
    # Read the contents of the file
    file
    |> File.read()
    |> rewrite(file, data)
    |> to_user_message()
    |> IO.puts
  end

  defp rewrite({:ok, content}, file, data) do
    content
    |> Poison.decode()
    |> write_decoded_content(file, data)
  end
  defp rewrite({:error, reason}, _, _) do
    {:error, {:on_read, reason}}
  end

  defp write_decoded_content({:ok, decoded_content}, file, data) do
    data
    |> Poison.encode()
    |> write_encoded_data(file, decoded_content)
  end
  defp write_decoded_content({:error, :invalid}, _, _) do
    {:error, {:on_decode_invalid, ""}}
  end
  defp write_decoded_content({:error, :invalid, reason}, _, _) do
    {:error, {:on_decode, reason}}
  end

  defp write_encoded_data({:ok, encoded_data}, file, decoded_content) do
    # Prepare the updated data for insertion
    # and encode the updated data
    decoded_content
    |> Map.update("data", encoded_data, fn _ -> encoded_data end)
    |> Poison.encode()
    |> write_encoded_final_data(file)
  end
  defp write_encoded_data({:error, {:invalid, reason}}, _, _) do
    {:error, {:on_encode_data, reason}}
  end

  defp write_encoded_final_data({:ok, encoded_final_data}, file) do
    file
    |> File.write(encoded_final_data)
    |> report_on_file_write()
  end
  defp write_encoded_final_data({:error, {:invalid, reason}}, _) do
    {:error, {:on_encode_final_data, reason}}
  end

  defp report_on_file_write(:ok),
    do: :ok
  defp report_on_file_write({:error, reason}),
    do: {:error, {:on_file_write, reason}}

  defp to_user_message(:ok),
    do: "Successfully updated file!"
  defp to_user_message({:error, {source, reason}}),
    do: to_error_msg source, reason

  defp to_error_message(source, reason) do
    case source do
      :on_read ->
        "Could not read file because #{reason}!"
      :on_decode_invalid ->
        "Could not parse file to JSON!"
      :on_decode ->
        "Could not parse file to JSON because #{reason}!"
      :on_encode_data ->
        "Could not encode given JSON because #{reason}!"
      :on_encode_final_data ->
        "Could not encode updated JSON because #{reason}!"
      :on_file_write ->
        "Could not write to file because #{reason}!"
    end
  end
end

And ultimately “error management” ignores the fact that not all kinds of errors can be predicted.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
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
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
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

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
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
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
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews