How to handle error handling boilerplate

Thank you for you detailed answer!
Yes, this is almost what I was talking about. I’ll provide an example to show what I meant.
Let’s say that CRUD operation is not the only thing I need to do during the request handling. And let’s say there are more than 2 layers in the application. So Controller calls a function from SurfaceLogic module. And this function calls another one from DeepLogic module. DeepLogic module works with some CRUDs and external APIs through clients.

#DeepLogic module
def do_deep_logic() do
  with {:ok, response} <- ExternalApiClient.get_info(), # can return  {:error, :api_500_error}
         {:ok, _} <- CRUD.insert() # can return {:error, changeset}
  do
      {:ok, do_some_logic(response)} 
  else
     {:error, %Changeset{ ... } = changest} -> {:error, :already_exists}
     {:error, reason} -> {:error, reason}
  end
end
# SurfaceLogic module
def do_surface_logic() do
  #pattern match on {:ok} and just pass all the errors to the upper level:
  with {:ok, value} <- DeepLogic.do_deep_logic() do
     {:ok, do_some_response_preparations(value)}
  end
end
#Controller
def handle_post_request(conn, _) do
  case SurfaceLogic.do_logic() do
    {:ok, result} -> render( ... ) # render success
    {:error, :already_exists} -> render (...) # render already exists
    {:error, :api_500_error} -> render ( ... ) # something wrong with external data provider
  end
end

So now I need to have case/with on every single layer here. (Yes, I still have action_fallback on controller level, but it will just move matching from this controller to Fallback Controller, so I’ll leave it here now to make it easier to understand).

My point here is that all these case/with statements look like boilerplate that you need to have everywhere (on every single function call, if this function returns :ok/:error). And I just try to understand if it is common practice (considered as ok) or not.

A more natural way to handle this issue for me would raise exceptions on the level of CRUD and ExternalApiClient and catch them on the controller level. In this case there wouldn’t be a need in case/with on the level of DeepLogic and SurfaceLogic. But it seems like this way is more uncommon in Elixir/FP community.