6. Errors (Result/Option and ?)¶
Incan uses explicit error values (Result and Option) instead of Python-style exceptions.
Result[T, E]¶
Use Result for operations that can fail.
def read_username() -> Result[str, str]:
name = input("Name: ").strip()
if len(name) == 0:
return Err("name must not be empty")
return Ok(name)
Handle it with match:
def main() -> None:
match read_username():
case Ok(name): println(f"hello, {name}")
case Err(e): println(f"error: {e}")
Option[T]¶
Use Option for “value may be absent”:
Option[T] has two variants:
Some(value)— value is presentNone— value is absent
You create a present value by wrapping it: Some(x).
def first(items: List[str]) -> Option[str]:
if len(items) == 0:
return None
return Some(items[0])
You handle it with match by destructuring Some(...).
def main() -> None:
match first(["a", "b"]):
case Some(x): println(f"first={x}")
case None: println("empty")
Coming from Python?
In Python typing, you’d usually express “may be missing” as:
from typing import Optional
def first(items: list[str]) -> Optional[str]:
return items[0] if items else None
In Python, Optional[T] is mostly a type-hinting/tooling concept. In Incan, Option[T] is an explicit enum that the compiler can reason about and enforce.
Propagating errors with ?¶
The ? operator returns early on Err, otherwise unwraps the Ok value:
def greet_user() -> Result[None, str]:
name = read_username()?
println(f"hello, {name}")
return Ok(None)
Structured errors (recommended)¶
Prefer structured errors over strings when the caller should branch on error kinds:
enum NameError:
Empty
def normalize(name: str) -> Result[str, NameError]:
cleaned = name.strip()
if len(cleaned) == 0:
return Err(NameError.Empty)
return Ok(cleaned.lower())
Try it¶
- Write
def safe_div(a: float, b: float) -> Result[float, str]. - Write
def first(items: List[str]) -> Option[str]and handleNonevsSome(...)withmatch. - Write
def greeting_for(name: str) -> Result[str, str]that uses?to propagate a validation error from anormalize_namehelper.
One possible solution
def safe_div(a: float, b: float) -> Result[float, str]:
if b == 0.0:
return Err("division by zero")
return Ok(a / b)
def first(items: list[str]) -> Option[str]:
if len(items) == 0:
return None
return Some(items[0])
def normalize_name(name: str) -> Result[str, str]:
cleaned = name.strip()
if len(cleaned) == 0:
return Err("name must not be empty")
return Ok(cleaned.lower())
def greeting_for(name: str) -> Result[str, str]:
cleaned = normalize_name(name)?
return Ok(f"hello, {cleaned}")
def main() -> None:
match first(["a", "b"]):
Some(value) => println(f"first={value}")
None => println("empty")
match greeting_for(" Ada "):
Ok(greeting) => println(greeting)
Err(error) => println(f"error: {error}")
match safe_div(10.0, 2.0):
Ok(value) => println(f"10 / 2 = {value}")
Err(error) => println(f"error: {error}")
The builtin float(text) conversion raises a runtime conversion error for malformed text; it does not return Result. The exercise therefore uses an explicitly fallible helper so the ? example matches the type system.
What to learn next¶
- Choosing between plain values,
Option,Result, and panics: Fallible and infallible paths - Results, Options, and the
?operator (deep dive): Error Handling - Common error message patterns: Error Messages