Why does EK9 have a built-in Money type and how does it handle currency safety?

← Getting Started · Ref: Q149

EK9 makes Money a built-in type because money is almost always implemented incorrectly in other languages. This is not a theoretical concern — it is the norm across the industry.

COMMON MONEY BUGS IN OTHER LANGUAGES

  - Using float or double: 0.1 + 0.2 != 0.3 in IEEE 754
  - Using BigDecimal but forgetting the rounding mode on one operation out of hundreds
  - Using BigDecimal with the double constructor: new BigDecimal(0.1) produces 0.100000000000000005551... not 0.1
  - Hardcoding 2 decimal places everywhere, then failing for JPY (0 decimals), BHD (3 decimals), or CLF (4 decimals)
  - Storing currency code separately from amount, then mixing them up during refactoring
  - Silently adding GBP to USD and getting a plausible but completely wrong number

These bugs are among the most expensive in software because they go undetected for months — the numbers look close enough until an audit finds millions in discrepancies.

MIXED CURRENCY RETURNS UNSET

Adding GBP to USD does not throw an exception. It returns an unset Money value:

  mixed <- 10#GBP + 30#USD    mixed is unset (mixed? is false)
  compare <- 10#GBP == 30#USD  compare is unset (not true, not false)

This follows the EK9 tri-state pattern. The compiler cannot catch this at compile time because variables could hold any currency at runtime. Instead, mixed currency operations return unset — forcing you to check before using the result.

DIVISION BY ZERO RETURNS UNSET

  bad <- 100#GBP / 0    bad is unset (bad? is false)

Operations that cannot produce a meaningful result return unset rather than crashing.

WHAT EK9 ELIMINATES

The literal 10#GBP encodes amount, currency, and precision in a single expression. The compiler knows all ISO 4217 currencies and their correct decimal places. Rounding is automatic and consistent (HALF_UP). Mixed currency operations return unset instead of silently producing wrong answers. There is nothing to forget, nothing to configure, and no way to accidentally use the wrong precision.

See Q35 for Money arithmetic and literals. See Q150 for currency conversion and locale formatting. See Q29 for the tri-state (absent/unset/set) model.

Example

defines module qa.money.safety

  defines program
    MoneySafetyDemo()
      stdout <- Stdout()

      tenPounds <- 10#GBP
      thirtyDollars <- 30.20#USD

      // === MIXED CURRENCY RETURNS UNSET ===

      // Cannot accidentally add GBP to USD — returns unset, not an exception
      mixedResult <- tenPounds + thirtyDollars
      require ~mixedResult?
      stdout.println(`Mixed currency isSet: ${mixedResult?}`)

      // Comparison across currencies also returns unset
      mixedCompare <- tenPounds == thirtyDollars
      require ~mixedCompare?
      stdout.println(`Mixed compare isSet: ${mixedCompare?}`)

      // === DIVISION BY ZERO RETURNS UNSET ===

      badResult <- tenPounds / 0
      require ~badResult?
      stdout.println(`Div by zero isSet: ${badResult?}`)

      // === SAME CURRENCY WORKS NORMALLY ===

      total <- tenPounds + 89.51#GBP
      require total?
      require total == 99.51#GBP
      stdout.println(`Same currency total: ${total}`)

      // === GUARD PATTERN FOR SAFE MONEY OPERATIONS ===

      // Use guard to safely handle potentially unset results
      if safeTotal <- tenPounds + 5#GBP
        stdout.println(`Safe total: ${safeTotal}`)

      // Mixed currency — guard body does NOT execute
      if unsafeTotal <- tenPounds + thirtyDollars
        stdout.println("This never prints")
      else
        stdout.println("Mixed currency detected via guard")

Common mistakes

E50060 — EK9 has no getCurrency() method on Money. Use the #> (extract-right) prefix operator to get the currency code as a String. AI models from Java and Python generate getter methods that do not exist in EK9. See ek9 -h E50060 for details.

Incorrect:

tenPounds.getCurrency()

Correct:

tenPounds + thirtyDollars

E50060 — EK9 Money uses operators for arithmetic, not method calls. There is no add() method. Use + for addition, - for subtraction. AI models transfer Java BigDecimal patterns that do not exist in EK9. See ek9 -h E50060 for details.

Incorrect:

tenPounds.add(89.51#GBP)

Correct:

tenPounds + 89.51#GBP

E50060 — EK9 has no getAmount() method on Money. Use the #< (extract-left) prefix operator to extract the numeric amount as a Float. AI models transfer Java BigDecimal getter patterns that do not exist in EK9. See ek9 -h E50060 for details.

Incorrect:

tenPounds.getAmount()

Correct:

tenPounds / 0

E50060 — EK9 Money handles rounding automatically using HALF_UP and the currency's ISO 4217 decimal places. There is no setScale() or round() method. GBP uses 2 decimals, JPY uses 0, CLF uses 4 — all handled automatically. See ek9 -h E50060 for details.

Incorrect:

tenPounds.setScale(2)

Correct:

tenPounds + 5#GBP
Other ways to ask this
  • Why not use BigDecimal or a money library?
  • What happens when I add GBP to USD in EK9?
  • How does EK9 prevent money bugs that plague other languages?
  • Why is Money built into EK9 instead of being a library?

Coming from another language?

Java: BigDecimal with explicit RoundingMode at every operation, Currency class separate from amount, mixed currency not detected. Python: decimal.Decimal requires context for rounding, no currency awareness, money libraries (py-moneyed) needed. JavaScript: IEEE 754 floating-point causes 0.1+0.2!=0.3 bugs, Dinero.js or similar needed. Ruby: no built-in, money gem needed. Go: no built-in, shopspring/decimal or similar. Rust: no built-in, rust_decimal crate. C#: decimal type has precision but no currency. EK9: built-in Money type eliminates every one of these mistakes at the language level.

Keywords: built-in, precision, unset, migrate, bigdecimal, rounding, currency, mixed, first, iso4217, intro, bug, money, financial, float, beginner, safety, start