Deference
A function deferring library inspired by zig!
The main purpose is chaining multiple API or 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, and throw_deferred/1.
Interface is not stable, but here's how to do it:
import Deference
def example() do
with_defer do
{:ok, user_id} =
API.User.create("really_cool_username", "really_cool_password")
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_deferred({:error, :failed_to_post})
end
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_deferred({:error, :failed_to_edit_post})
end
end
end
Deferred operations are collected and called in the reverse order they are called in.
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").
In case it's needed, with_defer_fwd/1 allows you to call deferred functions in the order specified.
If throw_deferred/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_deferred()
else
:ok
end
end
This will always resolve as :ok with no side effects.
throw_deferred/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_deferred({:error, :hello})
# some other stuff
:ok
end
Will always evaluate as {:error, :hello} stopping execution at the throw