Best practices when handling wrapped functions cases

Hi alchemists!

I have a question about best practices for returning values from a function that just forwards what another inner function does.

I wonder what are the best practices for handling cases like this. Should I just forward the returned value from the wrapped function or should I treat the returning cases even when by contract they are the same as the wrapping function?

@type on_work_fun :: {:ok, result} | {:error, term()}
@spec work_fun(any()) :: on_work_fun
def work_fun(arg) do
  ...
end

Then in another module…

@type on_wrapper_func :: {:ok, result} | {:error, term()}

@spec wrapper_fun(any()) :: on_wrapper_fun

# V1, just forward the wrapped function result (relies on the contract to be respected in the future)
def wrapper_fun(arg) do
  work_fun(arg)
end

# Keeps this function contract in any case
def wrapper_fun(arg) do
  case work_fun(arg) do
    {:ok, result} -> {:ok, result}
    {:error, reason} -> {:error, reason}
    error -> {:error, error}
  end
end

If your work_fun is typed correctly in the first place then, at least in the example you provided, it just can’t return anything other than what is specified by its type spec, so what’s the point of pattern matching on its returns? If your actual code looks like this you can even skip calling it and just use defdelegate:

defdelegate wrapper_fun(arg), to: SomeModule, as: :work_fun

If work_fun is breaking its contract, all this likely accomplishes is moving the crash site slightly - downstream code is almost certainly not expecting a shape like {:error, {:error, :what_the, :heck_is_this}}.

Dialyzer may also complain about this construct, as based on the declared typespecs that third branch of the case should be unreachable.

1 Like