moxley
I have a nested set of action calls, like this:
Action A → Action B
When the change function of Action B adds an error to its changeset, it somehow results in an uncaught throw in the call to Action B. This means that Action A can’t act on the return value from Action B when Action B returns an error.
Here’s approximately what the Action A change function might look like:
defmodule ResourceA.ActionA do
def change(changeset, _context) do
Ash.Changeset.before_action(changeset, fn changeset ->
result = ResourceB.action_b()
# If action_b() adds an error to its changeset,
# the result can't be acted upon here, because there is a throw generated internally.
changeset
end)
end
end
Here is an example repo that demonstrates the issue: hairbnb2/test/hairbnb/cart_test.exs at main · moxley/hairbnb2 · GitHub
I’ve tried using before_action, after_action, and manual, and all three techniques have the same issue.
Trending in Questions
Hello!
Suppose you are building workflow (order / task / payment) processing system with the following requirements:
Each workflow con...
New
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app?
Looking for hints regarding:
Addi...
New
Kia ora,
We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
Hi all, I wanted to ask how the community is dealing with post-release steps.
Today we have Ecto migrations, which make sure that the db...
New
Hello,
I have an Elixir backend that implements a custom protocol over TCP. I want to load test the backend and assess the performance o...
New
Other Trending Topics
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve.
They are GUI (Emerge) and State management (S...
New
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #phoenix_html
- #iex
- #graphql
- #ai
- #genstage
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex










First 8 of 8 Posts
zachdaniel
Yeah, so any errors that happen inside of action hooks will incur a rollback (the throwing behavior you’re describing), and the primary reason for this is that this is actually a situation that can be forced by some of the errors that can come back from a data layer. So for example, if you violate a unique constraint in the db, the transaction has to be aborted (and so you’d want an
after_transactionhook in place on the resource that started the transaction to handle/correct that.If you are aware that an action you’re calling may fail in a hook, you can use the
rollback_on_error?: falseoption when calling the action.With all of that said, an argument could be made that that is not the best design/doesn’t follow the principle of least surprise. We could introduce a backwards compatibility config that will modify the default of that option to
falseinstead relatively easily, which would make it such that only specific errors going wrong would rollback the transaction.moxley
I didn’t know about
rollback_on_error?: false. Here’s the documentation, if anyone’s curious.The final problem I’m encountering is that when Action B fails, it has an after_transaction callback that writes the error to the database. When calling Action B directly, this works as expected. If instead I call Action A, which calls Action B, then the error written to the database gets rolled back, because the after_transaction is itself inside an Action A’s transaction, which gets rolled back (this is typical SQL behavior– not just Ash). I can pass
rollback_on_error?: falsein Action A’s call to Action B, but other unexpected things happen: records that should get rolled back in Action B no longer get rolled back.I think that primarily because unexpected things will happen otherwise, the
rollback_on_error?: truedefault is the right choice.The fact that my application writes an error to the database may seem strange, but it’s because the execution flow is very asynchronous (User synchronous interaction with the API + Stripe WebHook callback + User WebSocket listening for updates based on the WebHook handling). The solution might be to delegate the error write to a separate process, which will be outside of the transaction of Action A and Action B.
zachdaniel
Yeah, so whenever you have something like this:
it typically represents something you want to rethink. Most often you’d do that by handling the error in an after_transaction hook on the calling action.
moxley
Actually, this still isn’t working.
It’s fine that all the nested SQL transactions get rolled back when Action B returns an error. The problem is that I need the code execution to continue without a throw– just as if I were calling Action B directly. Simply nesting an action call inside another shouldn’t change the behavior of the error case. Should I attempt to catch the throw, so that execution can continue?
zachdaniel
The problem is that you are in an invalid transaction. If you try to do another database operation in that rolled back transaction it will fail. So when something rolls back the transaction, it goes all the way back up to the place that started the transaction, and only that error logic gets a chance to run.
moxley
Understood. I’m not trying to do another database operation– not anymore. But what I want is the behavior of Action B to be the same whether it’s called directly or called from inside the transaction of Action A. Action B needs to handle errors it encounters, and it can’t do that if there is a throw coming from somewhere inside a call it’s making– a throw that only started happening when Action B was wrapped in Action A’s transaction.
What seems to work for now is to add a try+catch around the call that Action B is making. If Action B is wrapped in Action A’s transaction, the the catch will execute, and it can handle the error. If Action B is called directly, it handle the error in a more traditional way.
moxley
Here’s the approximate solution in Action B’s change module:
zachdaniel
I get what you’re saying, the fundamental issue here is that Ash doesn’t know what the wrapping transaction is going to try to do after the error occurs. The fundamental issue here is around how
after_transactionhooks work. If you didn’t start the transaction, then yourafter_transactionswon’t be the one to compensate for errors that your action returns from within the scope of what would be its own transaction. Remember that there is no such thing as a nested transaction (there are savepoints but that’s a separate conversation).But this is the point of the
rollback_on_error?option. You should be able to specifyrollback_on_error?: falseand then handle whatever error occurs from your action, assuming the error was not an error directly from the data layer (in which case we always roll back).