Skip to content

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 present
  • None — 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)

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

  1. Write def safe_div(a: float, b: float) -> Result[float, str].
  2. Write def first(items: List[str]) -> Option[str] and handle None vs Some(...) with match.
  3. Write def parse_and_divide(n: float, s: str) -> Result[float, str] that uses ? to parse s into a float, then calls safe_div.
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 parse_and_divide(n: float, s: str) -> Result[float, str]:
    denom = float(s)?
    return safe_div(n, denom)

def main() -> None:
    match first(["a", "b"]):
        case Some(x): println(f"first={x}")
        case None: println("empty")

    match parse_and_divide(10.0, "2.0"):
        case Ok(x): println(f"10 / 2 = {x}")
        case Err(e): println(f"error: {e}")

What to learn next

Next

Back: 5. Modules and imports

Next chapter: 7. Strings and formatting