sezaru

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

sorentwo

Oban Core Team

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

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

sorentwo

Oban Core Team

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.

Where Next?

Popular in Questions Top

New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement