A custom Business Event subscriber that throws an unhandled exception doesn't get retried, doesn't get queued for later — it fails once, inline with whatever save triggered it, and unless you've built logging into the subscriber yourself, the only trace is whatever generic error the user saw on their screen.
The subscriber is not sandboxed from the save
This is the detail that surprises people coming from event-driven systems where a subscriber failure is isolated by design. In Acumatica, a custom subscriber runs as part of the same transaction that persisted the triggering record. An unhandled exception in the subscriber can surface as a save failure to the user, even though the record itself was otherwise valid — which is exactly backwards from what most integrations want. The subscriber should never be able to block or fail the business transaction it's reacting to.
Every external call, every database write outside the current transaction, every parse of a field that might be null — catch it, log it, and return. A business event subscriber that fails silently is invisible; a business event subscriber that fails loudly enough to block a save is worse. Neither is what you want — log loudly to somewhere someone actually looks, and let the save succeed regardless.
What "loud" actually means
Loud doesn't mean an exception bubbling up to the user. It means a row in a table someone monitors, or a trace entry that feeds an alert, written from inside the catch block before the subscriber returns normally. PXTrace.WriteError writes to the trace log Acumatica already collects, which is a reasonable minimum, but it's easy to miss unless something is actively watching it — see the monitoring article for what "actively watching" should mean in practice.
public void OnBusinessEvent(SOShipment row)
{
try
{
EnqueueOutboundEvent(row);
}
catch (Exception ex)
{
PXTrace.WriteError($"Business event enqueue failed for {row.ShipmentNbr}: {ex}");
PXDatabase.Insert<BusinessEventFailureLog>(
new PXDataFieldAssign("RefNbr", row.ShipmentNbr),
new PXDataFieldAssign("ErrorMessage", ex.Message),
new PXDataFieldAssign("OccurredOn", DateTime.UtcNow));
// Deliberately not re-thrown: the shipment save must still succeed.
}
}
Separating enqueue failures from delivery failures
There are two distinct places things can go wrong, and they need different handling. Enqueue failures happen inside the subscriber itself — a bad field value, a database write that can't complete — and have to be caught defensively as above, because they're happening inline with a user's save. Delivery failures happen later, in whatever processor actually calls the external system, and those can and should be retried, because they're running on their own schedule with no user waiting on them. Conflating the two usually means retry logic ends up bolted onto the subscriber, which is the one place in this whole pattern that can't afford to be slow or fail loudly.
What to actually alert on
Not every logged failure needs a page. A single delivery failure that will be retried automatically doesn't need attention; a queue with a growing backlog of "Failed" rows after retry is exhausted does. Alert on the aggregate (row count in a Failed state past some threshold), not the individual event — otherwise a flaky downstream API turns into constant noise that trains everyone to ignore the alert.
Wrapping up
Error handling in Business Events is really two separate disciplines: defend the subscriber so it never blocks the save it's attached to, and give delivery failures somewhere real to land so they can be retried and, eventually, alerted on.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.