we-promise/sure · error · Money::ConversionError

Couldn't find exchange rate from #{from_currency} to #{to_cu

Error message

Couldn't find exchange rate from #{from_currency} to #{to_currency} on #{date}

What it means

Money#exchange_to looks up a rate via store.find_or_fetch_rate (ExchangeRate) for the given date; if no rate is found - or the rate is nil/zero/negative - it raises Money::ConversionError carrying from_currency, to_currency and date. custom_rate, when supplied, bypasses the lookup entirely.

Source

Thrown at lib/money.rb:65

  # Priority:
  #   1. Use custom_rate if explicitly provided (not nil)
  #   2. Look up rate via store.find_or_fetch_rate
  #   3. Raise ConversionError if no valid rate available
  def exchange_to(other_currency, date: Date.current, custom_rate: nil)
    iso_code = currency.iso_code
    other_iso_code = Money::Currency.new(other_currency).iso_code

    if iso_code == other_iso_code
      self
    else
      # Use custom rate if provided, otherwise look it up
      if custom_rate.present?
        exchange_rate = custom_rate.to_d
      else
        exchange_rate = store.find_or_fetch_rate(from: iso_code, to: other_iso_code, date: date)&.rate
      end

      raise ConversionError.new(from_currency: iso_code, to_currency: other_iso_code, date: date) unless exchange_rate && exchange_rate > 0

      Money.new(amount * exchange_rate, other_iso_code)
    end
  end

  def as_json
    { amount: amount, currency: currency.iso_code, formatted: format }.as_json
  end

  def <=>(other)
    raise TypeError, "Money can only be compared with other Money objects except for 0" unless other.is_a?(Money) || other.eql?(0)

    if other.is_a?(Numeric)
      amount <=> other
    else
      amount_comparison = amount <=> other.amount

      if amount_comparison == 0

View on GitHub (pinned to e69894adb9)

Solutions

  1. Pass custom_rate when you already know the rate (e.g. a rate captured at transaction time)
  2. Backfill the missing pair/date via the ExchangeRateSynthesizer/job so cross rates get derived
  3. Fallback to the nearest earlier date's rate before giving up (many call sites rescue ConversionError and skip/defer)
  4. Check for zero/negative corrupt rows in exchange_rates - a stored 0 also triggers this raise
  5. For weekends/holidays, synthesize the rate from the previous business day

Example fix

# before
usd = money.exchange_to("USD")

# after - explicit rate, else nearest-date fallback
usd = money.exchange_to("USD", custom_rate: captured_rate) if captured_rate
usd ||= begin
  money.exchange_to("USD", date: date)
rescue Money::ConversionError
  money.exchange_to("USD", date: ExchangeRate.where(from_currency: money.currency.iso_code, to_currency: "USD").order(date: :desc).first&.date)
end
Defensive patterns

Strategy: fallback

Validate before calling

rate = ExchangeRate.find_rate(from: from, to: to, date: date) rescue nil
rate ||= ExchangeRateSynthesizer.derive(from: from, to: to, date: date)
raise Money::ConversionError.new(from_currency: from, to_currency: to, date: date) unless rate&.positive_rate?

Type guard

def convertible?(money, to:, date: Date.current)
  money.currency.iso_code == Money::Currency.new(to).iso_code ||
    ExchangeRate.rate_exists?(from: money.currency.iso_code, to: to, date: date)
rescue Money::Currency::UnknownCurrency
  false
end

Try / catch

begin
  money.exchange_to("USD", date: date)
rescue Money::ConversionError => e
  Rails.logger.warn("Missing rate #{e.from_currency}->#{e.to_currency} on #{e.date}; using nearest")
  nearest = ExchangeRate.latest_before(e.from_currency, e.to_currency, e.date)
  nearest ? money.exchange_to("USD", custom_rate: nearest.rate) : skip_conversion(money)
end

Prevention

When it happens

Trigger: Converting a balance/transaction between currencies for a date with no stored or synthesizable rate: weekend/holiday dates where markets were closed; exotic pairs with no direct or cross rate (e.g. NZD to TRY); dates older than the exchange-rate history in the database; provider rate fetch disabled or failed so nothing was persisted.

Common situations: Portfolio net-worth syncing across many currencies where one minor pair lacks data; back-filling historical balances before the first ExchangeRate record; Yahoo/sync job failures leaving gaps in the rates table; custom date ranges in reports hitting sparse dates.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/cf2afa34112eded5. Report an issue: GitHub.