phacility/phabricator · error · Exception
Trying to refund a charge which is already refunding!
Error message
Trying to refund a charge which is already refunding!
What it means
Inside a read-locked transaction, willRefundCharge() reloaded the charge and found refundingPHID already set, meaning another refund is already in flight for this charge. This is the single-flight concurrency guard for refund initiation. A second refund may only start after didRefundCharge() or didFailRefund() clears the flag.
Source
Thrown at src/applications/phortune/storage/PhortuneCart.php:352
$refund_charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setPaymentMethodPHID($charge->getPaymentMethodPHID())
->setRefundedChargePHID($charge->getPHID())
->setAmountAsCurrency($amount->negate());
$charge->openTransaction();
$charge->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($copy->getRefundingPHID() !== null) {
throw new Exception(
pht('Trying to refund a charge which is already refunding!'));
}
$refund_charge->save();
$charge->setRefundingPHID($refund_charge->getPHID());
$charge->save();
$charge->endReadLocking();
$charge->saveTransaction();
return $refund_charge;
}
public function didRefundCharge(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_CHARGED);View on GitHub (pinned to 5720a38cfe)
Solutions
- Catch the exception and reload the charge: if refundingPHID is set, reuse or wait for the in-flight refund rather than retrying immediately.
- Make refund submission idempotent in the UI (disable after click, deduplicate requests).
- Only retry the provider apply step on failure, never the willRefundCharge() call.
Example fix
// before
$refund = $cart->willRefundCharge($actor, $provider, $charge, $amount);
// on retry/timeout this throws 'Trying to refund a charge which is already refunding!'
// after
try {
$refund = $cart->willRefundCharge($actor, $provider, $charge, $amount);
} catch (Exception $ex) {
$fresh = id(new PhortuneChargeQuery())
->setViewer($viewer)
->withPHIDs(array($charge->getPHID()))
->executeOne();
if ($fresh->getRefundingPHID()) {
return; // refund already in flight; do not retry
}
throw $ex;
} Defensive patterns
Strategy: try-catch
Validate before calling
$fresh = id(new PhortuneChargeQuery())
->setViewer($viewer)
->withPHIDs(array($charge->getPHID()))
->executeOne();
if ($fresh->getRefundingPHID() !== null) {
// a refund is already in flight; skip or wait for it
} Type guard
function isRefundInFlight(PhortuneCharge $charge) {
return $charge->getRefundingPHID() !== null;
} Try / catch
try {
$refund = $cart->willRefundCharge($actor, $provider, $charge, $amount);
} catch (Exception $ex) {
if (strpos($ex->getMessage(), 'already refunding') !== false) {
// another refund holds the lock: reload and resync with its outcome
} else {
throw $ex;
}
} Prevention
- Never blind-retry willRefundCharge(); the first attempt may still be completing.
- Make refund submission single-flight in the UI (disable on submit, dedupe requests).
- Retry only the provider apply step after failures, not the whole sequence.
When it happens
Trigger: Double-submitting a refund request (double-click, retry after a timeout); two admins or workers calling willRefundCharge() for the same charge before the first refund completes; retrying the whole refund sequence instead of just the provider step.
Common situations: Flaky network causing the browser to retry the refund POST; background jobs and UI issuing refunds concurrently; queue retries of a task that already opened a refund.
Related errors
- Charge has no transaction ID!
- Unable to refund charge; no Stripe chargeID!
- Trying to refund non-positive amount of money!
- Trying to refund more money than remaining on charge!
- Trying to refund a refund!
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/b1c9dfc685e2f668.
Report an issue: GitHub.