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

  1. Reload before acting: pledge.reload, then skip when !pledge.status_open? instead of raising
  2. Rescue GoalPledge::NotOpenError in background jobs and treat it as a benign no-op race (same pattern the reconciler uses for AlreadyClaimedError)
  3. 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

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.