Errors, error handling, and context managers
Mojo represents errors as values—specifically, as alternate return values from
functions. Unlike stack-unwinding exceptions in languages like C++ or Java, Mojo
errors don't require expensive call stack unwinding, so their runtime overhead is as
low as returning and checking an extra Bool. This design also enables error
handling in contexts where traditional exceptions aren't available, like GPU
kernels.
This page covers:
- Raise an error — Use the built-in
Errortype to raise errors with string messages. - Handle an error — Use
try/except/else/finallyto detect and recover from errors. - Typed errors — Define custom error types as structs for structured error data and compile-time type checking.
- Representing multiple error conditions — Use
enumerated error types or the
Varianttype for pattern matching. - The
Nevertype — Mark functions that always raise or never raise. - Parametric raises — Write generic functions that propagate error types from their arguments.
fnvsdeffor error handling — How the two function forms differ in their error handling capabilities.- Typed and
Errorinteraction — Work with code that uses both error styles. - Stack traces — Enable stack trace collection for debugging.
- Context managers — Manage resources safely
with the
withstatement.
An error interrupts the normal execution flow of your program. If you provide an
error handler (using try/except) in the current
function, execution resumes with that handler. If the error isn't handled in the
current function, it propagates to the calling function, and so on. If an error
isn't caught by any handler, your program terminates with a non-zero exit code
and prints the error message:
Unhandled exception caught during execution: record not foundRaise an error
The built-in Error type is the default error
type for most Mojo code. It carries a text message describing what went wrong,
and it's the right choice for application-level error handling—simple,
well-supported, and sufficient for the majority of use cases.
You can raise an Error with the constructor or a string literal shorthand:
# These are equivalent
raise Error("file not found")
raise "file not found"The string literal form is a convenience—the compiler automatically wraps it in
an Error.
A def function is raising by default and always uses the built-in Error type.
You don't include raises in the signature (doing so results in a
compiler error):
def read_file(path: String) -> String:
if not path:
raise "path cannot be empty"
return "contents of " + pathAn fn function can also raise Error by using bare raises without
a type:
fn read_file_fn(path: String) raises -> String:
if not path:
raise "path cannot be empty"
return "contents of " + pathHandle an error
Mojo uses try/except to detect and handle errors. The full syntax is:
try:
# Code that might raise an error
except e:
# Runs if an error occurs
else:
# Runs if no error occurs
finally:
# Always runs, regardless of outcomeYou must include one or both of except and finally. The else clause is
optional.
How each clause works
-
try— Contains code that might raise an error. If no error occurs, the entire block executes. If an error occurs, execution stops at theraisepoint and continues with theexceptclause (if present) or thefinallyclause. -
except— Runs only when an error occurs in thetryblock. If you provide a variable name (except e:), the error is bound to that variable. Atryblock can have only oneexceptclause. -
else— Runs only when no error occurs in thetryblock. Theelseclause is skipped if thetryclause exits viacontinue,break, orreturn. -
finally— Runs after thetryand anyexceptorelseclause, regardless of outcome. It executes even if another clause exits viacontinue,break,return, or by raising a new error. Usefinallyto release resources (such as file handles) that must be cleaned up regardless of whether an error occurred.
Example
The following example demonstrates all four clauses. The process_record()
function raises Error for different conditions, and the caller loops over
a list of IDs to exercise each clause:
fn process_record(id: Int) raises -> String:
if id < 0:
raise Error("invalid record ID: must be non-negative")
if id > 999:
raise Error("record not found")
return String("record_", id)
def main():
try:
for id in [5, 0, 1001, -3, 42]:
var result: String
try:
print()
print("try => id:", id)
if id == 0:
continue
result = process_record(id)
except e:
if "invalid" in String(e):
print("except => fatal:", e)
raise e^
print("except => handled:", e)
else:
print("else => success:", result)
finally:
print("finally => done with id:", id)
except e:
print("\nre-raised error:", e)
try => id: 5
else => success: record_5
finally => done with id: 5
try => id: 0
finally => done with id: 0
try => id: 1001
except => handled: record not found
finally => done with id: 1001
try => id: -3
except => fatal: invalid record ID: must be non-negative
finally => done with id: -3
re-raised error: invalid record ID: must be non-negativeNotice:
- When
idis 5:process_record()succeeds, soelseruns, thenfinally. - When
idis 0:continueexits thetryblock, skipping bothexceptandelse. Onlyfinallyruns. - When
idis 1001:process_record()raises an error. Theexceptclause handles it and execution continues with the next iteration. - When
idis -3:process_record()raises an "invalid" error. Theexceptclause re-raises it withraise e^, which transfers ownership of the error to the outertry/except. Thefinallyclause still runs before the error propagates. Because the re-raise exits the loop,id42 is never processed.
Re-raise an error
To re-raise a caught error, use raise with the
transfer sigil
(^) to transfer ownership of the error value:
try:
result = process_record(-1)
except e:
print("Logging error:", e)
raise e^ # re-raise with ownership transferYou can also raise a different error from within an except clause.
Typed errors
For code that needs more than a string message—like standard library APIs, GPU abstractions, or situations where callers need structured error data—Mojo lets you define custom error types as structs.
Define a custom error type
In Mojo, any struct can serve as an error type—no special base class or trait
is required. However, implementing the
Writable trait is recommended so the error
produces a readable message when printed or when the program terminates with an
unhandled error:
@fieldwise_init
struct ValidationError(Copyable, Writable):
var field: String
var reason: String
fn write_to(self, mut writer: Some[Writer]):
writer.write("ValidationError(", self.field, "): ", self.reason)The @fieldwise_init decorator
generates an __init__() method with an argument for each field, so you
can construct errors like ValidationError("username", "too short") or with
keyword arguments like ValidationError(field="username", reason="too short").
Raise a typed error
To declare that a function can raise a typed error, add raises YourErrorType
to its signature:
fn validate_username(username: String) raises ValidationError -> String:
if len(username) == 0:
raise ValidationError(field="username", reason="cannot be empty")
if len(username) < 3:
raise ValidationError(
field="username", reason="must be at least 3 characters"
)
return usernameA function defined with fn is non-raising by default. Including raises
(with or without a type) makes it a raising function. Each fn function can
declare at most one error type. The compiler enforces this—if any raise
statement in the function body doesn't match the declared type, the program
won't compile.
If a non-raising fn calls a raising function, it must handle the error
locally:
# This doesn't compile — validate_username() can raise
fn process_name(name: String):
print(validate_username(name))
# This compiles — the error is handled
fn process_name_safe(name: String):
try:
print(validate_username(name))
except e:
print("Invalid:", e)Catch a typed error
Use try/except to catch a typed error. The compiler automatically infers the
error type from the function being called, so except e: gives you a fully
typed error value—no casting required:
try:
var name = validate_username("")
except e:
# e is a ValidationError — access fields directly
print("Error in field '" + e.field + "': " + e.reason)Which produces this output:
Error in field 'username': cannot be emptyA try block can include only one except clause. Mojo doesn't support
except ErrorType as e: syntax—the type is always inferred from the
function being called.
If you need to handle calls that raise different error types, use separate
try blocks:
# Each try block handles one error type
try:
var name = validate_username(input)
except e:
# e is a ValidationError — access fields directly
print("Validation failed:", e.field, e.reason)
try:
var file = open_file(path)
except e:
# e is a FileError — match on variant
if e == FileError.not_found:
print("Missing:", path)Representing multiple error conditions
Each fn function can declare only one error type in its raises clause. When
a function can fail in multiple distinct ways, you need to represent those
conditions within a single type. Mojo offers two approaches:
- Enumerated error types — A single struct with
comptimevariant aliases. Simpler and more efficient when you only need to distinguish between conditions. - The
Varianttype — The standard libraryVarianttype with separate structs per condition. More flexible when each condition needs to carry different data.
Enumerated error types
A single struct can represent all error conditions using an integer _variant
field and
comptime values
as named constants. The write_to method generates human-readable strings from
the variant code:
@fieldwise_init
struct FileError(Equatable, ImplicitlyCopyable, Writable):
var _variant: Int
# Compile-time constant variants
comptime not_found = FileError(_variant=1)
comptime permission_denied = FileError(_variant=2)
comptime already_exists = FileError(_variant=3)
fn variant_name(self) -> String:
if self._variant == 1:
return "not_found"
elif self._variant == 2:
return "permission_denied"
elif self._variant == 3:
return "already_exists"
return "unknown"
fn write_to(self, mut writer: Some[Writer]):
writer.write("FileError.", self.variant_name())Because FileError has a single Int field and conforms to
Equatable, the compiler
auto-synthesizes __eq__(), so you can compare variants directly:
fn open_file(path: String) raises FileError -> String:
if not path:
raise FileError.not_found
if path == "/secret":
raise FileError.permission_denied
return "Contents of " + pathYou can then match on specific variants in the handler:
try:
print(open_file("/secret"))
except e:
if e == FileError.not_found:
print("Not found:", e)
elif e == FileError.permission_denied:
print("Permission denied:", e)Which produces this output:
Permission denied: FileError.permission_deniedThe Variant type
When each error condition needs to carry different data, you can use the
standard library Variant type instead of
an integer-based enumeration. Define a separate struct for each condition, then
combine them into a single error type with a comptime alias:
from utils import Variant
@fieldwise_init
struct NotFoundError(Copyable, Writable):
var path: String
fn write_to(self, mut writer: Some[Writer]):
writer.write("file not found: ", self.path)
@fieldwise_init
struct PermissionError(Copyable, Writable):
var path: String
var required_role: String
fn write_to(self, mut writer: Some[Writer]):
writer.write(
"permission denied on ",
self.path,
" (requires ",
self.required_role,
")",
)
comptime FileError = Variant[NotFoundError, PermissionError]Construct a Variant by wrapping the inner error in the Variant type:
fn open_file(path: String) raises FileError -> String:
if not path:
raise FileError(NotFoundError(""))
if path == "/secret":
raise FileError(PermissionError("/secret", "admin"))
return "Contents of " + pathIn the handler, use .isa[T]() to test which condition occurred and e[T] to
access the inner error with its full type:
try:
print(open_file("/secret"))
except e:
if e.isa[NotFoundError]():
print("Not found:", e[NotFoundError])
elif e.isa[PermissionError]():
print("Access denied:", e[PermissionError])Access denied: permission denied on /secret (requires admin)Use the Variant approach when each condition carries different fields (like
path vs path + required_role above). Use the
enumerated error type pattern when you only need to
distinguish between conditions without carrying different data per condition.
The Never type
Never is a type with no constructors—it can't be instantiated. This makes
it useful in error-handling signatures to express two opposite guarantees:
raises YourErrorType -> Never— The function always raises and never returns a value. This is useful for functions likepanic()that unconditionally signal an error.raises Never -> ReturnType— The function never raises and always returns a value. This is equivalent to omittingraisesentirely.
Functions that always raise
A function with -> Never as its return type must never terminate with a
return statement—it must raise on every code path (or loop infinitely).
Because Never can substitute for any type, the compiler allows using such a
function in place of a return value:
# Always raises, never returns
fn panic(msg: String) raises -> Never:
raise Error(msg)
fn get_value_or_panic(maybe: Optional[Int]) raises -> Int:
if maybe:
return maybe.value()
# Never substitutes for Int in this branch
panic("value is missing")Functions that never raise
A function with raises Never guarantees at compile time that it never raises.
This is equivalent to writing a plain non-raising function:
# These two signatures are equivalent:
fn safe_add(a: Int, b: Int) raises Never -> Int:
return a + b
fn safe_add(a: Int, b: Int) -> Int:
return a + bThis equivalency is especially useful in combination with
parametric raises, where the compiler infers
raises Never when a function argument doesn't raise.
Parametric raises
You can write generic functions that propagate the error type from a function argument to the caller. This uses a compile-time parameter for the error type:
fn run_action[
ErrorType: AnyType
](action: fn() raises ErrorType -> Int) raises ErrorType -> Int:
return action()The ErrorType parameter is inferred from the function you pass in. If the
function raises NetworkError, then run_action raises NetworkError. If the
function raises ParseError, then run_action raises ParseError:
fn fetch_data() raises NetworkError -> Int:
raise NetworkError(code=404)
fn parse_config() raises ParseError -> Int:
raise ParseError(position=42)
# ...
# ErrorType inferred as NetworkError
try:
_ = run_action(fetch_data)
except e:
print("Network failure:", e)
# ErrorType inferred as ParseError
try:
_ = run_action(parse_config)
except e:
print("Parse failure:", e)If the function argument doesn't raise at all, the compiler infers Never as
the error type. This means run_action itself becomes non-raising, and no try
block is needed:
fn get_value() -> Int:
return 99
# ...
# ErrorType inferred as Never — no try block needed
var result = run_action(get_value)
print("Got value:", result)Which produces this output:
Got value: 99fn vs def for error handling
Mojo has two function forms — def and fn — that differ in how they handle
errors:
def— Implicitly raising. Always uses the built-inErrortype. You can't specify a typed error.fnwith bareraises— Explicitly raising. Uses the built-inErrortype, same asdef.fnwithraises YourErrorType— Explicitly raising with a typed error. This is the only way to raise typed errors.
def functions
A def function can raise the built-in Error type without declaring it in the
signature:
def validate_def(value: Int) -> Int:
if value < 0:
raise "value cannot be negative"
return valueBecause def already implies raises, you can't add a typed error annotation.
The following produces a compiler error:
# Compiler error: function effect 'raises' was already specified
def validate_def(value: Int) raises ValidationError -> Int:
...fn functions
An fn function is non-raising by default. To make it raise, add raises
(for Error) or raises YourErrorType (for a typed error):
# fn with typed error — caller gets full access to error fields
fn validate_fn(value: Int) raises ValidationError -> Int:
if value < 0:
raise ValidationError(field="value", reason="cannot be negative")
return valueWrapping def in fn for typed errors
If you have existing def functions that you want to expose with typed
errors, write an fn wrapper that catches the Error and converts it:
fn validated_operation(value: Int) raises ValidationError -> Int:
try:
return validate_def(value)
except e:
raise ValidationError(field="value", reason=String(e))The wrapper calls the def function inside a try block, catches any
Error, and re-raises it as a typed error. This pattern lets you add type
safety at API boundaries without rewriting internal code:
try:
_ = validated_operation(-1)
except e:
print(e.field) # prints: value
print(e.reason) # prints: value cannot be negativeTyped errors and Error interaction
Most codebases contain a mix of def functions (which use Error) and
fn functions with typed errors. This section covers how the two styles
interact and how to work with them effectively.
Wrap Error at API boundaries
When calling def functions or other Error-raising code from a function that
uses typed errors, catch the Error and convert it:
def validate_with_error(value: Int) -> Int:
if value < 0:
raise "value cannot be negative"
return value
fn wrapped_validate(value: Int) raises ValidationError -> Int:
try:
return validate_with_error(value)
except e:
raise ValidationError(field="value", reason=String(e))This is the same wrapping pattern described in
fn vs def for error handling, and it's
the recommended way to integrate Error-based code into typed error APIs.
Avoid bare raises with typed errors
Using bare raises (without a type) on an fn that calls typed-error
functions causes type erasure—the compiler forgets the specific error type,
even though the runtime preserves the error's identity:
# Anti-pattern: bare raises erases type info at compile time
fn validate_bare_raises(value: Int) raises -> Int:
return validate_typed(value)The caller of validate_bare_raises() receives an Error, not a
ValidationError:
try:
_ = validate_bare_raises(-5)
except e:
# e is typed as Error — no field access available
# e.field would not compile here
print(e)ValidationError(value): cannot be negativeThe error message still shows ValidationError because the runtime preserves
the original error's Writable output. But the
compiler sees only Error, so you lose access to structured fields. Always use
raises YourErrorType to maintain type safety.
def functions can catch typed errors
A def function can call a function that raises a typed error and catch it
with full field access:
def error_caller():
try:
_ = validate_typed(-5)
except e:
# e is a ValidationError — field access works
print("Field:", e.field, "Reason:", e.reason)Executing this function produces this output:
Field: value Reason: cannot be negativeHowever, if a def function calls code that raises a typed error without
catching it, the typed error is subject to type erasure as described in
Avoid bare raises with typed errors.
Don't mix error types in a single try block
You can't call functions that raise different error types in the same try
block. The compiler rejects the mismatch:
def error_func() -> Int:
raise "something went wrong"
fn typed_func() raises ValidationError -> Int:
raise ValidationError(field="x", reason="invalid")
# This doesn't compile
fn mixed() raises ValidationError:
try:
_ = error_func() # raises Error
_ = typed_func() # raises ValidationError
except e:
print(e)The compiler reports:
error: cannot call function that may raise 'Error' in a context that
supports an error type of 'ValidationError'To call both functions, use separate try blocks or wrap the Error-raising
function as shown in
Wrap Error at API boundaries.
Recommendations for mixed codebases
When working with both Error and typed errors:
- Wrap at boundaries — Use
fnwrappers to convertErrorto typed errors at module boundaries, rather than rewriting all internal code at once. - Use
raises YourErrorType— Always specify the error type infnsignatures. Bareraisesdiscards type information. - Keep
deffor prototyping —defis convenient for quick prototyping, but convert tofnwith typed errors for production APIs. - Use separate
tryblocks — When calling functions with different error types, use nested or sequentialtryblocks to handle each type independently.
Enable stack trace generation for errors
Because Mojo represents errors as alternate return values rather than stack-unwinding exceptions, stack trace collection isn't automatic. Collecting a stack trace requires heap allocation and adds runtime overhead, so it's disabled by default to keep error handling lightweight.
By default, Mojo generates a stack trace when your program hits a segmentation
fault. You can disable this by setting the
MOJO_ENABLE_STACK_TRACE_ON_CRASH environment variable to 0 or false
(case-insensitive).
However, Mojo doesn't generate a stack trace when your program raises an
error—this avoids the additional runtime overhead. To enable stack traces for
raised errors, set the MOJO_ENABLE_STACK_TRACE_ON_ERROR environment variable
to any value other than 0 or false (case-insensitive).
Keep in mind that when you compile your program with
mojo build, the compiler optimizes and strips symbols by
default, so often your stack trace won't be very useful.
Consider this program:
def func2() -> None:
raise Error("Intentional error")
def func1() -> None:
func2()
def main():
func1()If you compile the program with default settings and run it with the environment variable set, you'll see a stack trace without symbols:
mojo build stacktrace_error.mojoMOJO_ENABLE_STACK_TRACE_ON_ERROR=1 ./stacktrace_error#0 0x... llvm::sys::PrintStackTrace(llvm::raw_ostream&, int)
#1 0x... KGEN_CompilerRT_GetStackTrace
#2 0x... main (./stacktrace_error+...)
Unhandled exception caught during execution: Intentional errorTo generate a more useful stack trace, compile the program with
--debug-level full (or -g) to include debug symbols:
mojo build --debug-level full stacktrace_error.mojoMOJO_ENABLE_STACK_TRACE_ON_ERROR=1 ./stacktrace_error#0 0x... llvm::sys::PrintStackTrace(llvm::raw_ostream&, int)
#1 0x... KGEN_CompilerRT_GetStackTrace
#2 0x... Error.__init__[...](...) .../builtin/error.mojo:159:38
#3 0x... stacktrace_error::func2() stacktrace_error.mojo:14:16
#4 0x... stacktrace_error::func1() stacktrace_error.mojo:18:10
#5 0x... stacktrace_error::main() stacktrace_error.mojo:22:10
#6 0x... __wrap_and_execute_raising_main[...](...) .../builtin/_startup.mojo:88:18
#7 0x... main .../builtin/_startup.mojo:103:4
Unhandled exception caught during execution: Intentional errorWith debug symbols, the trace shows the function call chain and source
locations: main() → func1() → func2() → Error.__init__().
Capture a stack trace programmatically
You can bind the Error instance to a variable in the except clause and
call its
get_stack_trace() method
to get the stack trace as an Optional[String]. The method returns None if
stack trace collection was disabled or unavailable:
def func2() -> None:
raise Error("Intentional error")
def func1() -> None:
func2()
def main():
try:
func1()
except e:
print(e)
print("-" * 20)
var stack_trace = e.get_stack_trace()
if stack_trace:
print(stack_trace.value())
else:
print("No stack trace available")When you compile with debug symbols and run with stack trace generation enabled:
mojo build --debug-level full stacktrace_error_capture.mojoMOJO_ENABLE_STACK_TRACE_ON_ERROR=1 ./stacktrace_error_captureIntentional error
--------------------
#0 0x... llvm::sys::PrintStackTrace(llvm::raw_ostream&, int)
#1 0x... KGEN_CompilerRT_GetStackTrace
#2 0x... Error.__init__[...](...) .../builtin/error.mojo:159:38
#3 0x... stacktrace_error_capture::func2() stacktrace_error_capture.mojo:14:16
#4 0x... stacktrace_error_capture::func1() stacktrace_error_capture.mojo:18:10
#5 0x... stacktrace_error_capture::main() stacktrace_error_capture.mojo:23:14
#6 0x... __wrap_and_execute_raising_main[...](...) .../builtin/_startup.mojo:88:18
#7 0x... main .../builtin/_startup.mojo:103:4Without enabling stack trace generation, the output is:
Intentional error
--------------------
No stack trace availableUse a context manager
A context manager is an object that manages resources such as files, network connections, and database connections. It provides a way to allocate resources and release them automatically when they are no longer needed, ensuring proper cleanup and preventing resource leaks even when errors occur.
As an example, consider reading data from a file. A naive approach might look like this:
# Obtain a file handle to read from storage
f = open(input_file, "r")
content = f.read()
# Process the content as needed
# Close the file handle
f.close()Calling close() releases the
memory and other operating system resources associated with the opened file. If
your program were to open many files without closing them, you could exhaust the
resources available to your program and cause errors. The problem is even worse
if you were writing to a file instead of reading from it, because the operating
system might buffer the output in memory until the file is closed. If your
program were to crash instead of exiting normally, that buffered data could be
lost instead of being written to storage.
The example above includes the call to close(), but it ignores the
possibility that read() could
raise an error, which would prevent the close() from executing.
To handle this scenario, you could rewrite the code to use try like this:
# Obtain a file handle to read from storage
f = open(input_file, "r")
try:
content = f.read()
# Process the content as needed
finally:
# Ensure that the file handle is closed even if read() raises an error
f.close()However, the FileHandle struct
returned by open() is a context manager.
When used with Mojo's with statement, a context manager ensures that the
resources it manages are properly released at the end of the block, even if an
error occurs. In the case of a FileHandle, that means the call to close()
takes place automatically. So you could rewrite the example above to take
advantage of the context manager (and omit the explicit call to close())
like this:
with open(input_file, "r") as f:
content = f.read()
# Process the content as neededThe with statement also allows you to use multiple context managers within the
same code block. As an example, the following code opens one text file, reads
its entire content, converts it to upper case, and then writes the result to a
different file:
with open(input_file, "r") as f_in, open(output_file, "w") as f_out:
input_text = f_in.read()
output_text = input_text.upper()
f_out.write(output_text)FileHandle is perhaps the most commonly used context manager. Other examples
of context managers in the Mojo standard library are
NamedTemporaryFile,
TemporaryDirectory,
BlockingScopedLock, and
assert_raises. You can also
create your own custom context managers, as described in Write a custom context
manager below.
Write a custom context manager
Writing a custom context manager is a matter of defining a
struct that implements two special dunder methods
("double underscore" methods): __enter__() and __exit__():
-
__enter__()is called by thewithstatement to enter the runtime context. The__enter__()method should initialize any state necessary for the context and return the context manager. -
__exit__()is called when thewithcode block completes execution, even if thewithcode block terminates with a call tocontinue,break, orreturn. The__exit__()method should release any resources associated with the context. After the__exit__()method returns, the context manager is destroyed.If the
withcode block raises an error, then the__exit__()method runs before any error processing occurs (that is, before it is caught by atry/exceptstructure or your program terminates). If you'd like to define conditional processing for error conditions in awithcode block, you can implement an overloaded version of__exit__()that takes an error argument. For more information, see Define a conditional__exit__()method and Handle typed errors in__exit__()below.For context managers that don't need to release resources or perform other actions on termination, you are not required to implement an
__exit__()method. In that case the context manager is destroyed automatically after thewithcode block completes execution.
Here is an example of implementing a Timer context manager, which prints the
amount of time spent executing the with code block:
import sys
import time
@fieldwise_init
struct Timer(ImplicitlyCopyable):
var start_time: Int
fn __init__(out self):
self.start_time = 0
fn __enter__(mut self) -> Self:
self.start_time = Int(time.perf_counter_ns())
return self
fn __exit__(mut self):
end_time = time.perf_counter_ns()
elapsed_time_ms = round(
Float64(end_time - UInt(self.start_time)) / 1e6, 3
)
print("Elapsed time:", elapsed_time_ms, "milliseconds")
def main():
with Timer():
print("Beginning execution")
time.sleep(1.0)
if len(sys.argv()) > 1:
raise "simulated error"
time.sleep(1.0)
print("Ending execution")Running this example produces output like this:
mojo context_mgr.mojoBeginning execution
Ending execution
Elapsed time: 2010.0 millisecondsmojo context_mgr.mojo failBeginning execution
Elapsed time: 1002.0 milliseconds
Unhandled exception caught during execution: simulated errorDefine a conditional __exit__() method
When creating a context manager, you can implement the __exit__(self) form of
the __exit__() method to handle completion of the with statement under all
circumstances including errors. However, you have the option of additionally
implementing an overloaded version that is invoked instead when an Error
occurs in the with code block:
fn __exit__(self, error: Error) raises -> BoolGiven the Error that occurred as an argument, the method can do any of the
following:
- Return
Trueto suppress the error. - Return
Falseto re-raise the error. - Raise a new error.
The following is an example of a context manager that suppresses only a certain error condition and propagates all others:
import time
@fieldwise_init
struct ConditionalTimer(ImplicitlyCopyable):
var start_time: Int
fn __init__(out self):
self.start_time = 0
fn __enter__(mut self) -> Self:
self.start_time = Int(time.perf_counter_ns())
return self
fn __exit__(mut self):
end_time = time.perf_counter_ns()
elapsed_time_ms = round(
Float64(end_time - UInt(self.start_time)) / 1e6, 3
)
print("Elapsed time:", elapsed_time_ms, "milliseconds")
fn __exit__(mut self, e: Error) raises -> Bool:
if String(e) == "just a warning":
print("Suppressing error:", e)
self.__exit__()
return True
else:
print("Propagating error")
self.__exit__()
return False
def flaky_identity(n: Int) -> Int:
if (n % 4) == 0:
raise "really bad"
elif (n % 2) == 0:
raise "just a warning"
else:
return n
def main():
for i in range(1, 9):
with ConditionalTimer():
print("\nBeginning execution")
print("i =", i)
time.sleep(0.1)
if i == 3:
print("continue executed")
continue
j = flaky_identity(i)
print("j =", j)
print("Ending execution")Running this example produces this output:
Beginning execution
i = 1
j = 1
Ending execution
Elapsed time: 105.0 milliseconds
Beginning execution
i = 2
Suppressing error: just a warning
Elapsed time: 106.0 milliseconds
Beginning execution
i = 3
continue executed
Elapsed time: 106.0 milliseconds
Beginning execution
i = 4
Propagating error
Elapsed time: 106.0 milliseconds
Unhandled exception caught during execution: really badHandle typed errors in __exit__()
The __exit__(self, error: Error) overload handles only Error values.
To handle typed errors, implement a generic __exit__() method with a
compile-time error type parameter:
fn __exit__[ErrType: AnyType](self, err: ErrType) -> BoolThis method receives the typed error directly, preserving its full type information. You can use reflection to inspect the error type at compile time. For example, you can:
get_type_name[ErrType]()— Get the error type's name as a string.@parameter if conforms_to(ErrType, Writable)— Check if the error implementsWritable.trait_downcast[Writable](err)— Access the error through itsWritableinterface.
The following ResourceGuard example demonstrates this pattern:
from reflection import *
@fieldwise_init
struct ConnectionError(Copyable, Writable):
var message: String
fn write_to(self, mut writer: Some[Writer]):
writer.write("ConnectionError: ", self.message)
struct ResourceGuard(ImplicitlyCopyable):
var name: String
var suppress_errors: Bool
fn __init__(out self, name: String, suppress_errors: Bool = False):
self.name = name
self.suppress_errors = suppress_errors
fn __enter__(self) -> Self:
print("Acquiring:", self.name)
return self
fn __exit__(self):
print("Releasing:", self.name, "(no error)")
fn __exit__[ErrType: AnyType](self, err: ErrType) -> Bool:
comptime type_name = get_type_name[ErrType]()
print("Releasing:", self.name)
print(" Error type:", type_name)
@parameter
if conforms_to(ErrType, Writable):
print(" Message:", trait_downcast[Writable](err))
return self.suppress_errorsWhen no error occurs, __exit__(self) runs as usual. When a typed error occurs,
__exit__[ErrType]() runs instead, giving you access to the error type and
its data:
# No error — calls __exit__(self)
with ResourceGuard("database"):
print("Working...")
# Typed error, suppressed — __exit__[ErrType] returns True
with ResourceGuard("cache", suppress_errors=True):
use_connection() # raises ConnectionError
print("Continued after suppressed error")Acquiring: database
Working...
Releasing: database (no error)
Acquiring: cache
Releasing: cache
Error type: ConnectionError
Message: ConnectionError: connection timed out
Continued after suppressed errorWas this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!