Skip to content

6. Errors (Result/Option and ?)

Chapter 6 of 13Result, 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 greeting_for(name: str) -> Result[str, str] that uses ? to propagate a validation error from a normalize_name helper.
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