sezaru
Is it possible to retrieve a Oban.PerformError from a failed job?
I’m trying to create a system where I will expose to end users the option to run tasks in the background. I want these users to be able to manage their tasks transparently.
This means that I want to use Oban to do that but I don’t want to expose anything related to oban to the end user.
So, for example, if the task finishes successfully, I can just use recorded: true and expose the recorded value to the end user instead of the full job structure.
This works fine for jobs that finished correctly, but not for jobs that failed to run, in these cases, what I tried to do was have an struct defined in a module, instantiate it in the job and return as {:error, struct}.
The issue with this is that Oban will convert this into an Oban.PerformError, and then store just the message value in the errors array of the job table, something like this:
errors: [
%{
"at" => "2026-02-04T23:18:39.876954Z",
"attempt" => 1,
"error" => "** (Oban.PerformError) Core.Workers.ApiCaller failed with {:error, %ErrorReply{http_code: 1, error: \"blobs\"}}"
},
%{
"at" => "2026-02-04T23:18:58.044722Z",
"attempt" => 2,
"error" => "** (Oban.PerformError) Core.Workers.ApiCaller failed with {:error, %ErrorReply{http_code: 1, error: \"blobs\"}}"
},
...
]
I don’t want to show this to the end user since it “exposes” oban, I just want the struct I created as the output.
Is there some way for me to achieve that without having to manage these states in a separated, custom, table?
Most Liked
sorentwo
You can define your own exception at the worker level, or externally if you want to share them between workers. Then you’ll raise that exception instead of letting it fall through to the generic Oban.PerformError exception. That way you can completely control the message:
defmodule MyApp.Worker do
use Oban.Pro.Worker
defmodule Error do
defexception [:message]
@impl true
def exception(value) do
message = "did not get what was expected, got: #{inspect(value)}"
%__MODULE__{message: message}
end
end
@impl true
def process(job) do
with {:error, reason} <- do_something(job.args) do
raise Error, reason
end
end
end
Since you’re in control of the message, you’ll have a predictable format for the error value, and you can show that or parse it out.
sezaru
Thanks, that works.
I’m not super happy with it since I will still need to extract the data I want from it using a regex which seems fragile, but at least I’m able to do that.
Here is the full solution:
defmodule Error do
defexception [:error]
@impl true
def exception(error) do
%__MODULE__{error: error}
end
def message(%{error: error}) do
error |> :erlang.term_to_binary() |> Base.encode64(padding: false)
end
def blame(exception, _stacktrace) do
{exception, []}
end
end
Then, when I retrieve the job, I will get something like this in the errors key:
errors: [
%{
"at" => "2026-02-05T16:09:52.916947Z",
"attempt" => 1,
"error" => "** (Core.Workers.ApiCaller.Error) g3QAAAADdwVlcnJvcm0AAAAFYmxvYnN3Cl9fc3RydWN0X193EUVsaXhpci5FcnJvclJlcGx5dwlodHRwX2NvZGVhAQ"
},
%{
"at" => "2026-02-05T16:10:10.055109Z",
"attempt" => 2,
"error" => "** (Core.Workers.ApiCaller.Error) g3QAAAADdwVlcnJvcm0AAAAFYmxvYnN3Cl9fc3RydWN0X193EUVsaXhpci5FcnJvclJlcGx5dwlodHRwX2NvZGVhAQ"
},
%{
"at" => "2026-02-05T16:10:29.159186Z",
"attempt" => 3,
"error" => "** (Core.Workers.ApiCaller.Error) g3QAAAADdwVlcnJvcm0AAAAFYmxvYnN3Cl9fc3RydWN0X193EUVsaXhpci5FcnJvclJlcGx5dwlodHRwX2NvZGVhAQ"
}
]
From this list, I can retrieve the data I want with:
def handle_error(%{errors: errors} = job) do
Enum.map(errors, fn %{"error" => error} ->
%{"base_64" => base_64} = Regex.named_captures(~r/(?<base_64>[A-Za-z0-9+\/=]+)$/, error)
base_64 |> Base.decode64!(padding: false) |> :erlang.binary_to_term()
end)
end
Which will return me the desired structs:
iex(86)> Core.Workers.ApiCaller.handle_error(j)
[
%ErrorReply{http_code: 1, error: "blobs"},
%ErrorReply{http_code: 1, error: "blobs"},
%ErrorReply{http_code: 1, error: "blobs"}
]
sorentwo
Raising in the before_process/1 hook isn’t advised; as you noted, that causes error logs because it’s intended for consistent pre-processing more than as a logic extension for process/1 callbacks.
There’s no reason an error/exception should be wrapped in Oban.PerformError as well. Returning {:error, exception} or {:cancel, exception} should just record the exception (though without a stacktrace, since there wasn’t a rescue involved).
That seems reasonable. Having a dedicated exception for each worker is something we considered before, actually.
Popular in Questions
Other popular topics
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
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex









