When should I use try/catch vs Result for error handling?

← Error Handling and Exceptions · Ref: Q139

EK9 provides two error handling approaches: try/catch for truly exceptional situations and Result for expected, recoverable failures. Choosing the right one depends on whether the error is expected or unexpected.

TWO ERROR HANDLING APPROACHES

Try/catch: For unexpected, exceptional conditions that disrupt normal flow.
Result: For expected failures that are part of normal operation.

WHEN TO USE TRY/CATCH

Use try/catch when:
- The error is truly unexpected (I/O failure, network down)
- The caller cannot meaningfully recover inline
- The error represents a broken invariant
- System-level failures that need stack unwinding
Example: File system errors, database connection failures, out of memory.

WHEN TO USE RESULT

Use Result when:
- Failure is an expected outcome (validation, lookup miss)
- The caller should handle both paths explicitly
- You want the compiler to enforce error checking
- The function signature should communicate that failure is possible
Example: User input validation, configuration lookup, parsing.

DESIGN GUIDELINES

1. If the caller must handle the error: use Result
2. If the error should propagate up: use try/catch
3. If failure is common (>5% of calls): use Result
4. If failure is rare and unexpected: use try/catch
5. For public APIs: prefer Result for clarity

EK9 PHILOSOPHY

EK9 encourages using Result for most application-level error handling because:
- The compiler enforces safe access (cannot access .ok() without guard)
- The function signature is explicit about failure possibility
- No hidden control flow jumps
Reserve try/catch for truly exceptional situations where Result would be awkward.

See Q48 for the Result type. See Q134 for basic try/catch. See Q86 for Result guard patterns. See Q87 for Result operations.

See Q304 for require preconditions. See Q305 for require vs assert vs throw.

Example

defines module qa.errorhandling.trycatchvsresult

  defines function

    <?-
      Validates an age value.
      Returns Result: ok with description, or error code.
      This is a RESULT approach - failure is expected and common.
    -?>
    validateAge()
      -> age as Integer
      <- rtn <- Result() of (String, Integer)

      maxAmount <- 150
      if age < 0
        rtn: Result(String(), -1)
      else
        if age > maxAmount
          rtn: Result(String(), -2)
        else
          rtn: Result("Valid: " + $age, Integer())

    <?-
      Reads configuration that might fail unexpectedly.
      Uses TRY/CATCH approach - failure is exceptional.
    -?>
    loadConfig()
      <- rtn <- String()

      // Simulating a config load that succeeds
      rtn: "production"

  defines program

    TryCatchVsResultDemo()
      stdout <- Stdout()

      // === RESULT APPROACH: Expected failures ===

      stdout.println("=== Result approach (validation) ===")

      ages <- List() of Integer
      ages += 25
      ages += -5
      ages += 200

      for age in ages
        result <- validateAge(age)
        if result?
          stdout.println("Valid age: " + $result.ok())
        else
          if result.isError()
            stdout.println(`Invalid (error code ${result.error()}) for age: ${age}`)

      // === TRY/CATCH APPROACH: Exceptional failures ===

      stdout.println("=== Try/catch approach (system errors) ===")

      try
        config <- loadConfig()
        stdout.println("Config loaded: " + config)
      catch
        -> ex as Exception
        stdout.println("System error: " + $ex)

      // === COMBINING BOTH ===

      stdout.println("=== Combined approach ===")

      try
        ageResult <- validateAge(30)
        if ageResult?
          stdout.println("Processing age: " + $ageResult.ok())
        else
          if ageResult.isError()
            stdout.println("Bad input: error code " + $ageResult.error())
      catch
        -> ex as Exception
        stdout.println("Unexpected error: " + $ex)

Common mistakes

E08180 — The return variable must be initialized. Using '<-' with a constructor ensures the variable starts initialized. Using 'as' without an initializer leaves it uninitialized, triggering E08180 — variable not marked for injection nor initialised. See ek9 -h E08180 for details.

Incorrect:

<- rtn as Result of (String, Integer)

Correct:

<- rtn <- Result() of (String, Integer)

E50060 — EK9 Result has no unwrap() method. Use ok() with a guard check (result?) or isOk(). See ek9 -h E50060 for details.

Incorrect:

stdout.println("Valid age: " + result.unwrap())

Correct:

stdout.println("Valid age: " + $result.ok())

E50060 — EK9 has no toString() method. Use string concatenation or interpolation instead. See ek9 -h E50060 for details.

Incorrect:

stdout.println(config.toString())

Correct:

stdout.println("Config loaded: " + config)

E50060 — EK9 Exception has no getMessage() method. Use reason() for the message or the $ operator for string conversion. See ek9 -h E50060 for details.

Incorrect:

stdout.println("System error: " + ex.getMessage())

Correct:

stdout.println("System error: " + $ex)

E50060 — EK9 Exception has no getMessage() method. Use the $ operator for string conversion. See ek9 -h E50060 for details.

Incorrect:

stdout.println("Unexpected error: " + ex.getMessage())

Correct:

stdout.println("Unexpected error: " + $ex)
Other ways to ask this
  • Should I use exceptions or Result in EK9?
  • What is the EK9 error handling philosophy?
  • When to throw vs return Result?

Coming from another language?

Java: exceptions for everything (checked and unchecked), no built-in Result type. Python: exceptions for everything including flow control (StopIteration). Rust: Result for all recoverable errors, panic! for unrecoverable. Go: error return values for everything, no exceptions. Kotlin: exceptions plus kotlin.Result for functional style. EK9: both try/catch AND Result available, philosophy favours Result for expected failures and try/catch for truly exceptional conditions, compiler enforces safe access on both.

Keywords: handle, catch, approach, ok, debug, philosophy, error, when, handling, strategy, design, exception, guard, choose, result