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