3
votes

I am trying to handle an error coming in a result of an HTTPoison.request! with

try do
  %{"session_id" => session_id} = ElixirDropbox.Files.UploadSession.start(client, true, image_save_path)
  write_sessional_values(session_id, file_size, "/#{construction}/#{camera_exid}/#{id}/#{starting}.jpg", path)
  check_1000_chunk(path) |> length() |> commit_if_1000(client, path)
rescue
  _ ->
    :timer.sleep(:timer.seconds(3))
    upload(200, response, starting, camera_exid, id, requestor)
end

my question is: I am totally ignoring the exception in rescue and doing the operation again which I want to do.

same as in with..

with {:ok, file_size} <- get_file_size(image_save_path),
     %{"session_id" => session_id} <- ElixirDropbox.Files.UploadSession.start(client, true, image_save_path) do             
  write_sessional_values(session_id, file_size, "/#{construction}/#{camera_exid}/#{id}/#{starting}.jpg", path)
  check_1000_chunk(path) |> length() |> commit_if_1000(client, path)
else
  _ ->
    :timer.sleep(:timer.seconds(3))
    upload(200, response, starting, camera_exid, id, requestor)        
end

I am totally ignoring what is coming in else part in with what are the solid grounds here to use both in which cases?

2

2 Answers

2
votes

with is used when you want to make sure that a number of steps ensure that the right data is available in your application before you can perform the task. A common example would be to make sure that fields exist in a map:

with {:ok, width} when not is_nil(width) <- Map.fetch(data, :width),
     {:ok, height} when not is_nil(height) <- Map.fetch(data, :height) do
   {:ok, width * height}
else
  _error ->
    {:error, "This data does not have the correct values"}
end

You want to use rescue when an exception is thrown that you want to recover from. Here's an example of when you would want to use rescue with the same operation:

try do
  width = Map.fetch!(data, :width)
  height = Map.fetch!(data, :height)
  {:ok, width * height}
rescue
  %KeyError{message: message} ->
    {:error, message}
end

Generally speaking, I prefer to structure my applications in a way that does not use rescue blocks. I would like to know when an error is thrown in the apps that I write so that I can fix something. I believe errors should be expected and exceptions should be an indication that you have a bug.

However, if you're working with a third party library that throws exceptions that you can expect then using rescue blocks is just fine.

The elixir docs are pretty helpful when talking about errors https://elixir-lang.org/getting-started/try-catch-and-rescue.html#errors

In the example you have provided I would recommend that you use a with block because you are expecting that the upload might fail. Also, I would recommend that you specify a max_retries variable that, when reached, throws an exception so you're not infinitely trying to upload something.

On a side note, I believe the recommended way to rescue something is to do it in the function and not a try/rescue block like this:

def my_func(a) do
  do_something(a)
rescue
  %Exception{} ->
    {:error, "ERROR"}
end

I hope this helps.

Cheers!

0
votes

rescue is a very rare guest in OTP world. Usually, we raise if and only we want to follow famous “let is crash” ideology to make the supervision tree to do all the work on restarting the process for us. rescue comes only with some external libraries, when it might raise in a very unexpected way in a very undesired place (or when the logic requires early return, but we did not manage to write a proper code to handle the logic in a more structured way.)

HTTPoison.request!/5 ever exists to help you manage scrapers when you spawn 1M processes crawling the web, all supervised, and when one crashes, it gets restarted by its supervisor without you to need to write a line of code to handle retries.


Answering the question stated. HTTPoison has a very extensive error reporting, and by calling it without bang, one should examine the error reported in a case of unsuccessful request. If it was 404, there is zero sense in the retry. If it was 403, there is zero sense to retry immediately, rather one should follow the loop to retrive the proper credentials and only then retry.

If it was 500 or 503, one should probably wait longer to get a time for the server to recover.

Also, different types of errors should be probably logged differently.

So, yeah, do smth like

case HTTPoison.get(url) do
  {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
    process(body)
  {:ok, %HTTPoison.Response{status_code: 404}} ->
    Logger.warn("url not found")
  {:error, %HTTPoison.Error{reason: reason}} ->
    retry_upload
end