Handled exceptions

The bug that never crashes anything still needs a paper trail

Code that catches its own error to keep running reports nothing to an exception tracker by default; report it with real context instead, without changing how your code already handles it.

A rescue with nowhere to go

An SFTP upload to a partner, a webhook post, a call to a payment gateway: code that wraps a risky third-party call in its own rescue or try/catch so one flaky dependency can't take the whole request or job down with it. That's usually the right call, but it has a real cost: once the rescue clause finishes, nothing about that failure exists anywhere. No exception ever reached the tracker, because nothing ever crashed. The only way anyone finds out is by noticing the downstream effect later, or not noticing at all.

Grouping, alerting, and every other feature on this page already assume an exception reached ForgeOps somehow; this is what actually gets one there for the failures that were never going to raise on their own.


One call, right where you already caught it

Every SDK exposes a direct capture call for exactly this, made right inside the existing catch/rescue/except block: hand it the error you already have, plus whatever context actually matters for debugging this specific failure, and it reports on its own, with no change to how that block already behaves otherwise. Nothing framework-wide to install, nothing to opt into ahead of time; it's just one more line at the exact spot the error is already being handled.

See the real call for your own language below, not a generic pseudocode stand-in.


Context becomes inspectable JSON on the issue

Whatever's in that context hash, an internal record ID, a remote host, a file path, a retry count, is attached to that exact occurrence, not just logged somewhere else and hoped to be findable later. It shows up as pretty-printed, expandable JSON right on the issue's own occurrence card.

Scrubbed exactly the same way an unhandled exception's backtrace already is: email addresses, formatted SSNs and credit cards, known API key/token shapes, and anything under a suspiciously named key are redacted before the payload ever leaves the process, then scrubbed again on arrival regardless.

An issue's occurrence card with its Context section expanded, showing a JSON payload describing a failed SFTP upload to a partner

A caught SFTP failure, reported with the exact shipment, host, and attempt number attached.


Report it in your language

Ruby on Rails: swap a bare rescue for Rails.error.handle, Rails' own error-reporting convention, already subscribed to automatically:

Rails.error.handle(fallback: -> { nil }) do
  charge_card(order)
end

.NET:

catch (CardException ex)
{
    ForgeOpsTrackerSdk.Capture(ex, new Dictionary<string, object?> { ["order_id"] = order.Id });
}

Python:

except CardError as e:
    forge_ops_tracker.capture_exception(e, context={"order_id": order.id})

PHP:

} catch (CardDeclinedException $e) {
    ForgeOpsTracker::captureException($e, ['order_id' => $order->id]);
}

Node.js:

} catch (err) {
  forgeOpsTracker.captureException(err, { orderId: order.id });
}

Java:

} catch (CardDeclinedException e) {
    ForgeOpsTracker.captureException(e, Map.of("orderId", order.getId()));
}

C++:

} catch (const CardDeclinedException& e) {
    forge_ops_tracker::capture_exception(e, {{"order_id", order.id}});
}

Objective-C:

} @catch (NSException *exception) {
    [ForgeOpsTracker captureException:exception context:@{ @"orderId": order.identifier }];
}

Perl:

if ($@) {
    ForgeOps::Tracker::report($@, { order_id => $order->id });
}

Browser (TypeScript/JavaScript):

} catch (err) {
  forgeOpsTracker.captureException(err, { orderId: order.id });
}

Go: no exceptions, so an error you already have is reported explicitly, right where you'd otherwise just log it:

if err != nil {
    forgeops.CaptureError(err, map[string]any{"order_id": order.ID})
    return err
}

Rust: no exceptions either, so a Result::Err you already caught is reported the same way:

if let Err(err) = charge_card(&order) {
    forge_ops_tracker::capture_error(&err, forge_ops_tracker::context!{"order_id" => order.id});
    return Err(err);
}

Swift:

do {
    try chargeCard(order)
} catch {
    ForgeOpsTracker.capture(error: error, context: ["order_id": order.id])
}

Kotlin:

} catch (e: CardDeclinedException) {
    ForgeOpsTracker.captureException(e, mapOf("orderId" to order.id))
}

Dart/Flutter:

} catch (e, stackTrace) {
  forge_ops_tracker.captureException(e, stackTrace, context: {'orderId': order.id});
}

C: no exceptions at all, so an error your own code already detected (a bad return code) is reported explicitly:

if (charge_card(order) != 0) {
    forgeops_tracker_capture_error("PAYMENT_ERROR", "card declined", context_keys, context_values, 1);
}

Android (Kotlin):

} catch (e: CardDeclinedException) {
    ForgeOpsTracker.captureException(e, mapOf("orderId" to order.id))
    Log.w("Checkout", "card declined: ${e.message}")
}

React Native:

} catch (error) {
  forgeOpsTracker.captureException(error, { orderId: order.id });
}

Unity (C#):

catch (Exception e)
{
    ForgeOpsTrackerClient.CaptureException(e, new Dictionary<string, object> { ["orderId"] = order.Id });
}

Elixir: reported explicitly, typically right before re-raising:

rescue
  e ->
    ForgeOpsTracker.capture_exception(e, __STACKTRACE__, %{order_id: order.id})
    reraise e, __STACKTRACE__

See it on your own data

Free plan included, no credit card required.

Get started free