we-promise/sure · warning · GoalPledge::NotOpenError
Only open pledges can be extended
Error message
Only open pledges can be extended
What it means
GoalPledge#extend! raises NotOpenError unless status_open?. Pledges are a small state machine (open to matched/cancelled/expired); extending expires_at is only defined while the pledge is open.
Source
Thrown at app/models/goal_pledge.rb:121
end
# Valuation-backed match: no transaction to stamp, just flip the pledge.
def resolve_with_valuation!
with_lock do
raise NotOpenError, "Pledge no longer open" unless status_open?
update!(status: "matched")
end
end
class NotOpenError < StandardError; end
# Raised when a Transaction is already claimed by a different open
# pledge. Lets the reconciler distinguish a known race ("another worker
# got there first") from a generic validation failure.
class AlreadyClaimedError < StandardError; end
def extend!(days: EXTEND_DAYS)
raise NotOpenError, "Only open pledges can be extended" unless status_open?
update!(expires_at: expires_at + days.days)
end
def cancel!
raise NotOpenError, "Only open pledges can be cancelled" unless status_open?
update!(status: "cancelled")
end
def expire!
return unless status_open?
update!(status: "expired")
end
def days_left
return 0 unless status_open?View on GitHub (pinned to e69894adb9)
Solutions
- Reload before acting: pledge.reload, then skip when !pledge.status_open? instead of raising
- Rescue GoalPledge::NotOpenError in background jobs and treat it as a benign no-op race (same pattern the reconciler uses for AlreadyClaimedError)
- In the UI, disable the extend action for non-open statuses
Example fix
// before pledge.extend! // after pledge.reload pledge.extend! if pledge.status_open?
Defensive patterns
Strategy: validation
Validate before calling
pledge.reload.status_open? # false => extend! will raise NotOpenError
Type guard
def extendable?(pledge) = pledge.reload.status_open? # narrow before calling extend!
Try / catch
rescue GoalPledge::NotOpenError and treat as a benign race (another worker matched/cancelled first); log at info, not error
Prevention
- Reload pledges immediately before state-changing calls in concurrent flows
- Design pledge jobs to tolerate AlreadyClaimedError and NotOpenError as expected races
- Disable extend/cancel UI for non-open statuses
When it happens
Trigger: Calling pledge.extend!(days:) on a pledge whose status is matched, cancelled, or expired — commonly a concurrent worker matched or cancelled it between load and extend, or the UI acts on a stale pledge object.
Common situations: Race between the reconciler matching a pledge and a user/job extending it; double-click on an extend button; scheduled extend jobs picking up pledges that matched after enqueue.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/e319d155f781f2e7.
Report an issue: GitHub.