Mojo Error Handling: How raises Works and Why It Matters

Mojo error handling is not Python’s exception model with different syntax — it is a fundamentally different contract between the developer and the compiler. In Python, any function can raise any exception at any time and nothing in the type system warns you. In Mojo, a function that can raise an error must declare raises in its signature, and the compiler enforces this declaration across every call site. If you call a raises function without a try block and your caller does not declare raises, the code does not compile. This page covers the full error handling model: syntax, propagation mechanics, Python interop edge cases, and the production patterns that make Mojo error handling work for you instead of against you.

Written for Python developers moving into Mojo. Every section explains Mojo behavior by contrast with what Python does differently — and why the difference matters for production code.


TL;DR

  • Mojo uses explicit raises declarations — a function that can raise must say so in its signature or the compiler rejects it
  • Calling a raises function without try/except is a compile-time error, not a runtime surprise — unlike Python
  • def functions in Mojo implicitly allow raising — fn functions require explicit raises declaration
  • Errors propagate up the call stack automatically when callers declare raises — no re-raise boilerplate needed
  • Python interop is the messiest boundary — Python exceptions crossing into Mojo code require explicit handling at the interop layer
  • Custom error types in Mojo implement the Error trait — not subclassed from a base exception class like in Python

How Mojo Error Handling Differs from Python Exceptions

Python’s exception model is implicit and unchecked. Any function can raise any exception — ValueError, RuntimeError, a custom exception, anything — without declaring it in the function signature. The caller has no compile-time information about what might be raised. This is flexible but produces a class of bugs that only appear at runtime: unhandled exceptions that crash production code because nobody knew the function could raise in that scenario.

Mojo’s error handling model is explicit and compiler-enforced. A function that can raise an error must declare raises in its signature. The compiler tracks this declaration across every call site. If you call a raises function without wrapping it in try/except and your current function does not also declare raises, the compiler rejects the code before it ever runs. This eliminates an entire class of unhandled exception bugs at compile time — not at 3am in production.

# Mojo — raises declaration: compiler-enforced error contract
fn divide(a: Float64, b: Float64) raises -> Float64:
 if b == 0.0:
 raise Error("division by zero") # only valid because fn declares raises
 return a / b

fn safe_caller(): # does NOT declare raises
 let result = divide(10.0, 2.0) # compile error — raises fn called without try

Without the raises declaration on divide, the raise Error() line inside it would itself be a compile error — Mojo does not allow raising from a function that did not declare it can raise. This is the constraint that makes Mojo’s error model statically verifiable.

Does Mojo Have Exceptions?

Mojo has errors, not exceptions in the Python sense. The distinction matters: Python exceptions are objects that inherit from BaseException and carry a full class hierarchy. Mojo errors implement the Error trait and are simpler by design — they carry a message string and are designed for low-overhead error propagation in performance-critical code. The behavioral difference is significant: Python exceptions unwind the stack with full traceback capture overhead. Mojo errors propagate with minimal runtime cost, which matters in the AI inference and numerical computing workloads Mojo targets.

mojo raises keyword: What the Compiler Checks at Every Call Site

The raises keyword in a Mojo fn signature does two things. First, it permits the function body to contain raise statements and calls to other raises functions. Second, it signals to every caller that this function can produce an error that must be handled. The compiler uses this signal to enforce handling at every call site — either the caller wraps the call in try/except, or the caller declares raises itself and propagates the error upward. There is no third option. This is fundamentally different from Python where forgetting to handle an exception is always a runtime problem, never a compile-time one.

mojo try except: Syntax and Runtime Behavior

Mojo’s try/except syntax is deliberately close to Python’s — the learning curve for Python developers is minimal. The semantics differ in one critical way: in Mojo, try/except is not optional when calling a raises function from a non-raising context. The compiler requires it. In Python, try/except is always optional — you choose whether to handle or let the exception propagate. In Mojo, the choice is made explicit at the function signature level, not at the call site.

Deep Dive
Mojo Pitfalls Manual: MojoWiki

Beyond the Hype: The Unofficial MojoWiki for Production-Grade Engineering Mojo ships with a pitch that's hard to ignore: Python syntax, C-level performance, and MLIR power under the hood. While the Mojo programming language is a...

The snippet below shows a complete try/except block in Mojo with the pattern you will use in production code. The except clause receives the error value — use e.message to access the error string. The else clause runs only when no error was raised — this is the same semantics as Python’s try/except/else.

# Mojo — complete try/except pattern with error message access
fn process_data(value: Float64) raises -> Float64:
 if value < 0.0:
 raise Error("negative value not allowed: " + str(value))
 return value * 2.0

fn run():
 try:
 let result = process_data(-5.0) # raises because value is negative
 print("result:", result) # only runs if no error raised
 except e:
 print("caught error:", e) # e.message contains the error string

Without the try/except block around process_data(-5.0) in a non-raising run() function, this code does not compile. The compiler sees that process_data declares raises and run does not — unhandled error is a compile-time rejection, not a runtime crash.

How to Handle Errors in Mojo: The Basic Pattern

The standard pattern for mojo error handling in production code is: declare raises on functions that can fail, use try/except at the boundary where you want to handle the error, and let errors propagate through intermediate functions by declaring raises on them too. Handle errors at the level where you have enough context to do something useful — log, recover, return a default, or report to the caller. Do not catch errors just to re-raise them — that is Python boilerplate that Mojo’s propagation model makes unnecessary.

What Happens When a Mojo Function Raises

When a Mojo function raises an error at runtime, execution stops at the raise statement and the error propagates up the call stack. Each frame in the stack is an fn that declared raises — the compiler already verified this chain at compile time, so there are no surprises at runtime. The first try/except block encountered in the propagation chain catches the error. If no try/except catches it before the top of the stack, the program terminates with the error message. Unlike Python, there is no full traceback capture overhead during this propagation — Mojo’s error propagation is designed for the low-latency environments where Mojo is used.

mojo error propagation: The raises Chain

Error propagation in Mojo is explicit by design. When a function calls a raises function and wants to let the error propagate to its own caller instead of handling it locally, it declares raises in its own signature. The compiler verifies the entire chain at compile time — every link in the propagation chain must declare raises or the code does not compile. This gives you a complete static picture of which functions in your codebase can produce errors, traceable from the source to the handler without running the code.

The snippet below shows a three-level call chain where an error originates in the innermost function and propagates through two intermediate functions to the handler. Each intermediate function declares raises — no re-raise statements, no boilerplate, just the declaration.

# Mojo — error propagation through a call chain (no re-raise needed)
fn read_config(path: String) raises -> String:
 if path == "":
 raise Error("config path cannot be empty") # error originates here
 return "config_data"

fn initialize_system(config_path: String) raises -> String:
 return read_config(config_path) # propagates — no try/except needed here

fn main():
 try:
 let config = initialize_system("") # error propagates up to here
 except e:
 print("system init failed:", e) # caught at the top-level handler

Without raises on initialize_system, calling read_config inside it would be a compile error — the compiler sees that read_config raises and initialize_system neither handles it nor declares it can propagate it. This is the constraint that makes the error chain statically verifiable end to end.

How to Propagate Errors in Mojo Without Losing Context

Mojo’s propagation model carries the original error message through the entire call chain without modification — whatever string you passed to Error() at the raise site is what arrives at the handler. If you need to add context at an intermediate level (the equivalent of Python’s exception chaining with raise NewError() from original), the pattern is to catch the error, construct a new error with an enriched message, and re-raise. Use this sparingly — in most cases the original error message is sufficient and adding intermediate context clutters the error log without adding diagnostic value.

mojo fn raises: When to Declare It and When Not To

Declare raises on an fn when the function contains logic that can legitimately fail — invalid input, resource unavailability, a downstream call that can raise. Do not declare raises on pure computational functions where failure is not a valid outcome — mathematical operations on validated input, data transformations with guaranteed valid types. The discipline of not adding raises everywhere is what gives the keyword its value: when you see raises on a function, it means something real, not a defensive catch-all. Functions without raises are a compile-time guarantee that they cannot fail — that is a stronger contract than any comment or documentation.

Technical Reference
Mojo limitations

3 Mistakes Teams Make When Using Mojo for Backend Services and Web Development TL;DR: Quick Takeaways Mojo limitations 2026 are real — ecosystem maturity is nowhere near Python's. Treat it as a scalpel, not a...

Mojo Error Handling vs Python: What Python Developers Get Wrong

Python developers coming to Mojo make three consistent mistakes with error handling. The first is calling raises functions without try/except in non-raising contexts — this produces a compile error that can feel confusing if you are used to Python’s permissive model. The second is adding raises to every function defensively — this defeats the purpose of the explicit model and produces a codebase where raises means nothing. The third is mishandling errors at the Python interop boundary — where Python exceptions cross into Mojo code.

# Mojo — WRONG vs RIGHT: common Python developer mistakes

# WRONG: calling raises fn from non-raising fn without try/except
fn compute() -> Float64: # does not declare raises
 return divide(10.0, 0.0) # compile error — unhandled raises call

# RIGHT: either handle locally or declare raises to propagate
fn compute() raises -> Float64: # declares raises — propagates to caller
 return divide(10.0, 0.0)

# also RIGHT: handle locally when this is the appropriate boundary
fn compute_safe() -> Float64:
 try:
 return divide(10.0, 0.0)
 except e:
 return 0.0 # explicit fallback — no raises needed

The wrong pattern above is the most common first error Python developers hit in Mojo. In Python, forgetting to handle an exception is always valid syntax — the exception just propagates silently. In Mojo, it is a compile error with a clear message pointing to the exact call site. Once you internalize that the compiler is doing the work Python left to runtime, the error model feels like a feature rather than friction.

Calling a raises Function Without try — The Exact Compiler Error

When you call a raises function from a non-raising fn without a try/except block, the Mojo compiler produces an error at the call site. The message indicates that the called function can raise but the current function does not handle or propagate the error. The fix is always one of two options: wrap the call in try/except to handle locally, or add raises to the current function’s signature to propagate upward. Neither option is better universally — the right choice depends on whether the current function has enough context to handle the error meaningfully.

Python Interop and Error Handling: Where the Boundary Gets Messy

Mojo can call Python code via its Python interop layer. When Python code raises a Python exception, that exception crosses the Mojo boundary as an error. The interop layer wraps Python exceptions in Mojo’s error type — the error message contains the Python exception type and message. The practical implication: any Mojo function that calls Python code through the interop layer must declare raises, because Python exceptions are not statically predictable. This is the one place in Mojo where the raises declaration becomes a blanket requirement rather than a precise contract — Python’s unchecked exception model leaks through the interop boundary.

Mojo Error Handling Best Practices for Production Code

Mojo error handling in production code follows a different discipline than Python. Because the compiler enforces the error contract statically, the design decisions you make about where to raise, what to raise, and where to handle have more structural weight — they are part of your function’s public interface in a way that Python exceptions are not.

Raise at the earliest point where you have enough information. If a function receives invalid input, raise immediately with a message that identifies the invalid value. Do not propagate invalid data deeper into the call stack and raise later — the error message at the deep level will have less context about the original cause.

Handle at the highest level where you can do something useful. Catching an error in an intermediate function just to log and re-raise adds noise without value. Let errors propagate to the level where your code can recover, report to a user, or fail cleanly.

Use custom error types for domain errors. A string message in Error("something failed") is sufficient for simple cases. For production systems with multiple error categories, define custom types that implement the Error trait — this gives callers the ability to match on error type, not just parse message strings.

# Mojo — custom error type implementing the Error trait
struct ValidationError(Error):
 var field: String
 var message: String

 fn __init__(inout self, field: String, message: String):
 self.field = field
 self.message = message # satisfies the Error trait requirement

fn validate_email(email: String) raises:
 if "@" not in email:
 raise ValidationError("email", "missing @ symbol: " + email)

Without a custom error type, all errors in your system are indistinguishable at the handler — every except e block receives a generic Error and must parse the message string to determine the error category. Custom types make error handling at the boundary cleaner and more maintainable as the codebase grows.

Mojo Error Types: Defining Custom Errors

Custom error types in Mojo are structs that implement the Error trait. The trait requires a message field of type String — this is what the except e clause receives. Beyond the required field, you can add any fields your error type needs: error codes, affected field names, upstream error details, timestamps. Define custom error types for any error category that callers need to handle differently — not for every possible failure mode. The goal is enough granularity to handle errors intelligently at the boundary, not a class hierarchy that mirrors Python’s exception tree.

When to Handle and When to Propagate in Mojo

Handle an error locally when your function has the context and authority to recover or provide a meaningful fallback — a file not found error in a config loader that has a default config, a network timeout in a retry loop that can attempt again. Propagate an error when your function is too deep in the call stack to know what recovery means — a data validation failure in a parsing function, a resource allocation failure in an initialization routine. The rule: if handling the error at this level produces correct program behavior, handle it. If handling it here would hide a real failure from the caller, propagate it.

Worth Reading
Mojo Production Deployment Patterns

Mojo in Production: Hard Truths and Performance Gaps Missing from Official Docs Finding Mojo lang production deployment patterns that actually work requires looking exactly where the marketing benchmarks stop and real infrastructure begins. While the...

FAQ: Mojo Error Handling

How to handle errors in Mojo?

Use try/except around any call to a function that declares raises. The except e clause receives the error value — access the message with e.message or just print e directly. If you want to propagate the error instead of handling it, declare raises on your own function and omit the try/except — the compiler will verify the propagation chain is complete. Handle errors at the level where your code has enough context to recover or report meaningfully.

Does Mojo have exceptions like Python?

Mojo has errors, not exceptions in the Python sense. Python exceptions are objects in a class hierarchy that can be raised from any function without declaration. Mojo errors implement the Error trait and must be declared in the function signature with raises. The compiler enforces handling at every call site — uncaught errors in non-raising functions are compile-time errors, not runtime surprises. Mojo errors are also lower overhead than Python exceptions — no full traceback capture on raise.

What is the raises keyword in Mojo?

raises is a keyword added to an fn function signature to declare that the function can produce an error. It serves two purposes: it permits the function body to contain raise statements and calls to other raises functions, and it signals to every caller that this function can fail. The compiler uses this signal to enforce error handling at every call site. A function without raises is a compile-time guarantee that it cannot fail — a stronger contract than any comment.

How to propagate errors in Mojo?

Declare raises on the propagating function and call the inner raises function without a try/except block. The error propagates automatically up the call stack to the nearest try/except handler. No re-raise statements needed — unlike Python where you sometimes write except e: raise to propagate after logging. The compiler verifies the entire propagation chain at compile time, so a missing raises declaration anywhere in the chain is a compile error.

What is the difference between Mojo and Python error handling?

Python’s exception model is implicit — any function can raise anything without declaring it, and forgetting to handle an exception is always a runtime problem. Mojo’s model is explicit — functions that can raise must declare raises, and calling them without handling is a compile error. Python exceptions carry full traceback overhead on raise. Mojo errors propagate with minimal runtime cost. Python uses class hierarchies for exception types. Mojo uses structs implementing the Error trait.

Can a Mojo fn without raises call a function that raises?

Only if the call is wrapped in a try/except block. A non-raising fn can call a raises function as long as it handles the error locally — the try/except block satisfies the compiler’s requirement that the error is handled before it could escape to a non-raising context. If you call a raises function from a non-raising fn without try/except, the compiler rejects the code at that call site with a clear error message.

How to define custom error types in Mojo?

Define a struct that implements the Error trait. The trait requires a message field of type String. Add any additional fields your error type needs — error codes, affected field names, contextual data. Use custom error types when callers need to distinguish between error categories at the handler, not for every possible failure. A single generic Error with a descriptive message is sufficient for most internal errors — reserve custom types for errors that cross module or API boundaries.

What happens if an unhandled error reaches the top of the call stack in Mojo?

The program terminates and prints the error message. There is no default exception handler that catches and logs like Python’s interpreter-level handler. In practice, a well-structured Mojo program should have a try/except at the top-level entry point — typically in main() — to catch any errors that propagate to the top and handle them cleanly rather than crashing. This is the same pattern as wrapping Python’s if __name__ == "__main__" block in a top-level try/except.

Written by:

Source Category: Mojo Language