Mojo Custom Errors

Custom errors let you define specific, named error conditions that carry meaningful context about what went wrong. Instead of raising a generic Error("something failed"), you create domain-specific error types that communicate intent clearly — to other developers, to log systems, and to any code that needs to handle different failures differently.

Why Custom Errors Matter

Generic error:                     Custom error:
  raise Error("failed")              raise ValidationError("age", -5,
                                         "must be non-negative")

  Caller sees:                       Caller sees:
  "failed"                           field="age", value=-5,
  → What failed? Where? Why?         reason="must be non-negative"
  → Hard to handle programmatically  → Easy to log, display, retry

Building a Custom Error Type

Mojo does not yet have a built-in error hierarchy. The standard pattern is to create a struct that formats a descriptive message string, then raise a standard Error constructed from that message.

struct ValidationError:
    var field:  String
    var value:  String
    var reason: String

    fn __init__(inout self, field: String, value: String, reason: String):
        self.field  = field
        self.value  = value
        self.reason = reason

    fn to_error(self) -> Error:
        return Error(
            "ValidationError: field='" + self.field +
            "' value='" + self.value +
            "' reason='" + self.reason + "'"
        )

fn validate_age(age: Int) raises:
    if age < 0:
        raise ValidationError("age", String(age), "must be non-negative").to_error()
    if age > 150:
        raise ValidationError("age", String(age), "exceeds realistic maximum").to_error()

fn main():
    try:
        validate_age(-3)
    except e:
        print(str(e))
        # ValidationError: field='age' value='-3' reason='must be non-negative'

A Library of Domain Errors

Group related error types in one place so any module in your project can import and raise them consistently.

# File: errors.mojo

struct NotFoundError:
    var resource: String
    var id:       String

    fn __init__(inout self, resource: String, id: String):
        self.resource = resource
        self.id = id

    fn to_error(self) -> Error:
        return Error("NotFoundError: " + self.resource + " with id='" + self.id + "' not found")

struct PermissionError:
    var user:      String
    var operation: String

    fn __init__(inout self, user: String, operation: String):
        self.user = user
        self.operation = operation

    fn to_error(self) -> Error:
        return Error("PermissionError: user='" + self.user +
                     "' cannot perform '" + self.operation + "'")

struct RangeError:
    var param: String
    var value: Float64
    var lo:    Float64
    var hi:    Float64

    fn __init__(inout self, param: String, value: Float64, lo: Float64, hi: Float64):
        self.param = param
        self.value = value
        self.lo = lo
        self.hi = hi

    fn to_error(self) -> Error:
        return Error(
            "RangeError: " + self.param +
            "=" + String(self.value) +
            " not in [" + String(self.lo) + ", " + String(self.hi) + "]"
        )

Using the Error Library

fn get_user(user_id: String) raises -> String:
    if user_id == "":
        raise NotFoundError("User", user_id).to_error()
    if user_id == "admin":
        return "Alice (admin)"
    raise NotFoundError("User", user_id).to_error()

fn delete_user(user_id: String, caller: String) raises:
    if caller != "admin":
        raise PermissionError(caller, "delete_user").to_error()
    print("User", user_id, "deleted")

fn set_temperature(t: Float64) raises:
    if t < -273.15 or t > 10000.0:
        raise RangeError("temperature", t, -273.15, 10000.0).to_error()
    print("Temperature set to", t)

fn main():
    try:
        var user = get_user("unknown_id")
    except e:
        print(str(e))
        # NotFoundError: User with id='unknown_id' not found

    try:
        delete_user("user42", "guest")
    except e:
        print(str(e))
        # PermissionError: user='guest' cannot perform 'delete_user'

    try:
        set_temperature(-300.0)
    except e:
        print(str(e))
        # RangeError: temperature=-300.0 not in [-273.15, 10000.0]

Parsing Error Types from Messages

When a function can fail in multiple ways, the caller may need to distinguish error types. Embed a type tag in the message and check it in the except block.

fn classify_error(e: Error) -> String:
    var msg = str(e)
    if msg.startswith("ValidationError"):
        return "validation"
    elif msg.startswith("NotFoundError"):
        return "not_found"
    elif msg.startswith("PermissionError"):
        return "permission"
    else:
        return "unknown"

fn main():
    try:
        var user = get_user("")
    except e:
        var kind = classify_error(e)
        if kind == "not_found":
            print("Show 404 page")
        elif kind == "permission":
            print("Redirect to login")
        else:
            print("Show generic error:", str(e))
Error routing diagram:

  Function raises Error
         │
         ▼
  except e:
    classify_error(e)
         │
  ┌──────┼──────────┐
  ▼      ▼          ▼
 404  login      generic
 page redirect   handler

Error Context Builder

A context builder pattern adds location information — file, function, line — to any error message automatically.

fn with_context(e: Error, context: String) -> Error:
    return Error("[" + context + "] " + str(e))

fn load_config(path: String) raises -> String:
    try:
        if path == "":
            raise Error("path is empty")
        return "config data"
    except e:
        raise with_context(e, "load_config")

fn start_app() raises:
    try:
        var cfg = load_config("")
    except e:
        raise with_context(e, "start_app")

fn main():
    try:
        start_app()
    except e:
        print(str(e))
        # [start_app] [load_config] path is empty

Key Takeaways

Custom error types communicate the specific reason, location, and context of a failure rather than just "something failed." Build error structs with descriptive fields and a to_error() method that produces a formatted Error object. Group related error types in a dedicated module so every part of your project uses consistent error messages. Embed type tags at the start of error messages to enable programmatic classification in except blocks. Context builders wrap existing errors with location information as they propagate up the call stack, giving you a readable error trail without a stack trace.

Leave a Comment

Your email address will not be published. Required fields are marked *