I personally am very inclined to force explicitness and use more verbosity to describe flows. When using with I normally resort to tagging the branches, but sometimes you do need to pass through the underlying returns from the failing branch. When writing API endpoints I’ve settled in just having a %Response{}/%Output{} struct with :errors, :notices, :payload fields that the frontend knows how to interpret - this approach requires you to do it almost from the beginning and be in control also of how the front-end interacts. It’s also not completely feasible for html pipelines, as you can have redirects and so on, but could probably be made so.
Controllers/channels call only into context functions, and these entry points in the contexts either return those payload structs, or they return normal results and I have a translation module/functions for the returns. Again, it’s more boilerplaty.
But the issue that I think deserves a bit more thought is not that raising exceptions in themselves is bad (outside of things that really require raising, like a db disconnect, or inaccessibility of a resource, or inside a loop/recursion that no longer makes sense, but usually there you use throw), when you look at the flow of a single entry point, it can usually look clearer and in a way less verbose. The problem comes when all your code base applies this style. Then it basically becomes a mine field because everything can raise somewhere and you’re left having to keep in mind every detail of all functions you’re calling and what those themselves are calling.
When I have too much time to think about this nonsense I imagine I would like to see a variation of with (a special form named weave or steps) that would look like this:
weave # notice the new line
[not_found: %User{} = user] <- Db.Repo.get(User, user_id),
[not_authorized: true] <- Contexts.Authorization.is_authorized?(user, some_action),
[changeset: {:ok, %User{} = new_user}] <- Contexts.Users.update(user, some_action)
do
{:ok, user}
else
[changeset: changeset] -> {:error, changeset}
[{error, _}] -> {:error, error}
end
Perhaps it could even be that when the do block was ommitted it would pass the last value, without the keyword.
weave
[not_found: %User{} = user] <- Db.Repo.get(User, user_id),
[not_authorized: true] <- Contexts.Authorization.is_authorized?(user, some_action),
[changeset: {:ok, %User{} = new_user}] <- Contexts.Users.update(user, some_action)
else
[changeset: changeset] -> {:error, changeset}
[{error, _}] -> {:error, error}
end
This can be confusing the first time you encounter it (not_authorized: true) but afterwards it’s simple to understand.
Other option would be:
weave
%User{} = user <- Db.Repo.get(User, user_id) -> :not_found,
true <- Contexts.Authorization.is_authorized?(user, some_action) -> :not_authorized,
{:ok, %User{} = new_user} <- Contexts.Users.update(user, some_action) -> :changeset
else
[changeset: changeset] -> {:error, changeset}
[{error, _}] -> {:error, error}
end






















