Error handling recipes¶
This page is a practical companion to Error handling. It focuses on common patterns and “what to do when…”.
Coming from Python?
In Python, many failures surface as exceptions (try/except) and “propagation” is implicit.
In Incan, common failures are modeled explicitly with Result[T, E]:
- Use
?to propagate anErr(...)to your caller (similar to “let it raise”, but typed). - Use
matchto handle/recover locally (similar totry/except, but without hidden control flow). - Avoid
unwrap()on user/config/network inputs; it’s closer toassert Falsethan “normal error handling”.
Pattern: Handle vs propagate¶
Use ? when you want to propagate failures to the caller.
Use match when you want to recover, retry, branch, or attach context.
def load_user(id: int) -> Result[User, AppError]:
# Propagate: caller decides what to do.
user = fetch_user(id).map_err(AppError.Network)?
return Ok(user)
def load_user_or_guest(id: int) -> User:
# Recover: we decide locally.
match load_user(id):
Ok(user) => return user
Err(_) => return User.guest()
Pattern: Convert errors at module boundaries (map_err)¶
Convert low-level errors into a stable, module-level error type.
enum ConfigError:
Io(IoError)
Parse(str)
def load_config(path: Path) -> Result[Config, ConfigError]:
content = path.read_text().map_err(ConfigError.Io)?
cfg = parse_toml[Config](content).map_err(|e| ConfigError.Parse(e))?
return Ok(cfg)
Tip: do this once at the boundary, not at every call site.
Pattern: Turn Option[T] into Result[T, E] (ok_or)¶
Use this when “missing” should become a recoverable error.
def require_user(id: int) -> Result[User, AppError]:
return users.get(id).ok_or(AppError.NotFound(f"user {id}"))
Pattern: Defaults (unwrap_or / unwrap_or_else)¶
Use defaults only when there is a genuinely safe fallback.
timeout = settings.get("timeout_secs").unwrap_or("2.0")
If computing the default is expensive, prefer unwrap_or_else(...) when available.
Pattern: Retry a fallible operation¶
Keep retries local and explicit.
def fetch_with_retry(url: str, attempts: int) -> Result[str, NetworkError]:
mut last_err: Option[NetworkError] = None
for _ in range(attempts):
match fetch(url):
Ok(body) => return Ok(body)
Err(e) =>
last_err = Some(e)
continue
return Err(last_err.unwrap_or(NetworkError("unreachable")))
Pattern: Errors with recoverable payloads¶
Sometimes the error should carry a value that would otherwise be lost (e.g. sending on a closed channel).
match await tx.send(msg):
Ok(_) => println("Sent!")
Err(e) =>
save_for_retry(e.value)
“Don’t do this”: unwrap() on user input¶
unwrap() and panic() are for “should never happen” paths (tests, invariants, internal compiler bugs), not expected
failures.
Prefer Result and handle the error where you can recover.