Deference
A function deferring library inspired by zig!
The main purpose is chaining operations which could fail for any number of reasons, while
also requiring cleanup operations to be performed. The most important bits are with_defer/1, defer/1, err_defer/1, and throw_err/1.
import Deference
def example() do
with_defer do
{:ok, user_id} =
API.User.create("really_cool_username", "really_cool_password")
err_defer do
API.User.delete("really_cool_username")
end
post_id =
API.Post.create("really_cool_username", "hello")
|> case do
{:ok, post_id} -> post_id
{:error, _reason} ->
# can't post, no reason to keep the user around
throw_err({:error, :failed_to_post})
end
err_defer do
API.Post.delete(post_id)
end
API.Post.edit(post_id, "hello\nedit: wow i didn't expect this to blow up")
|> case do
{:ok, post_id} -> :ok
{:error, _reason} ->
# failed to edit post, bail!
throw_err({:error, :failed_to_edit_post})
end
end
end
Deferred operations are collected and called in the reverse order they are specified in, and they come in two flavors: Error defers and standard defers.
Error defers are only called if throw_err/1 is called or an exception occurs within the with_defer block. Standard defers are always called, even when the with_defer block exits normally.
In the case above, if the post failed to edit, then first it would call API.Post.delete(post_id), then API.User.delete("really_cool_username").
If throw_err/1 is not called, then the block returns as normal, e.g:
with_defer do
defer do
:logger.error("this shouldn't happen!")
end
if 1 == 2 do
throw_err()
else
:ok
end
end
This will always resolve as :ok with no side effects.
throw_err/1 also allows you an early return path. Whatever term is provided will be the return for the whole block, defaulting to :error.
with_defer do
# some stuff
throw_err({:error, :hello})
# some other stuff
:ok
end
Will always evaluate as {:error, :hello} stopping execution at the throw
A regular throw/1 will return early as well, but without executing err_defer'd statements.
Some options are provided for convenience:
a
rescueclause can be provided directly inwith_defer:with_defer do raise "Abort" rescue _ -> :saved endfwdwill call deferred functions in the order they were specified:with_defer fwd: true do defer do :logger.debug("1") end defer do :logger.debug("2") end defer do :logger.debug("3") end endwill result in 1, 2, 3 being logged.
safewill wrap the block in a try/call block, killing any exceptions:with_defer safe: true do raise "Abort" endwill return
{:error, :exception}I don't recommend using this, but it's there! Instead, consider providing a rescue clause.