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.
