Errors, error handling, and context managers
This page discusses how to raise errors in Mojo programs and how to detect and handle error conditions. It also discusses how you can use context managers to correctly allocate and release resources such as files, even when error conditions occur. Finally, it shows you how to implement context managers for your own custom resources.
Raise an error
The raise statement raises an error condition in your program. You provide the
raise statement with an Error instance
to indicate the type of error that occurred. For example:
raise Error("integer overflow")As a convenience, you can instead provide an error message in the form of a
String or
StringLiteral value, and
raise automatically uses that to create an Error instance. So you can raise
the same error condition as shown above by executing:
raise "integer overflow"An error interrupts the execution flow of your program. If you provide an error handler (as described in Handle an error) 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 error handler, your program terminates with a non-zero exit code and prints a stack trace, if enabled, followed by the error message. For example:
stack trace was not collected. Enable stack trace collection with environment variable `MOJO_ENABLE_STACK_TRACE_ON_ERROR`
Unhandled exception caught during execution: integer overflowEnable stack trace generation for errors
By default, Mojo generates a stack trace when your program hits a segmentation
fault. If you don't want this behavior, you can disable it 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—we skip this to avoid the additional run-time overhead. If you want stack
traces for raised errors, you can enable them by setting 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.
Let's look at this program:
def func2() -> None:
raise Error("Intentional error")
def func1() -> None:
func2()
def main():
func1()If you compile the program with the 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 0x0000000104ef8ecc llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xccecc)
#1 0x0000000104e379e4 KGEN_CompilerRT_GetStackTrace (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xb9e4)
#2 0x000000010478868c main (/Users/ken/tmp/stack/stacktrace_error+0x10000068c)
Unhandled exception caught during execution: Intentional errorTo generate a more useful stack trace, you need to 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 0x0000000102bf4ecc llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xccecc)
#1 0x0000000102b339e4 KGEN_CompilerRT_GetStackTrace (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xb9e4)
#2 0x000000010266468c stdlib::builtin::error::Error::__init__[__mlir_type.!kgen.string](::StringLiteral[$0])_REMOVED_ARG open-source/max/mojo/stdlib/stdlib/builtin/error.mojo:159:38
#3 0x000000010266468c stacktrace_error::func2()_REMOVED_ARG /Users/ken/tmp/stack/stacktrace_error.mojo:14:16
#4 0x000000010266468c stacktrace_error::func1() /Users/ken/tmp/stack/stacktrace_error.mojo:18:10
#5 0x000000010266468c stacktrace_error::main() /Users/ken/tmp/stack/stacktrace_error.mojo:22:10
#6 0x000000010266468c stdlib::builtin::_startup::__wrap_and_execute_raising_main[fn() raises -> None](::SIMD[::DType(int32), ::Int(1)],__mlir_type.!kgen.pointer<pointer<scalar<ui8>>>),main_func="stacktrace_error::main()" open-source/max/mojo/stdlib/stdlib/builtin/_startup.mojo:88:18
#7 0x000000010266468c main open-source/max/mojo/stdlib/stdlib/builtin/_startup.mojo:103:4
Unhandled exception caught during execution: Intentional errorAs described in Handle an error, you can bind the Error
instance to a variable in the except clause. If you do, you can invoke its
get_stack_trace() method
to get a StackTrace instance.
StackTrace implements the Stringable
trait, so you can construct a String with String(stack_trace) if you want to
extract the stack trace as a String for further processing. For example:
def func2() -> None:
raise Error("Intentional error")
def func1() -> None:
func2()
def main():
try:
func1()
except e:
print(e)
print("-" * 20)
print(String(e.get_stack_trace()))When you compile this program with symbols and run it with stack trace generation enabled, you'll see the following output:
mojo build --debug-level full stacktrace_error_capture.mojoMOJO_ENABLE_STACK_TRACE_ON_ERROR=1 ./stacktrace_error_captureIntentional error
--------------------
#0 0x0000000102d24ecc llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xccecc)
#1 0x0000000102c639e4 KGEN_CompilerRT_GetStackTrace (/Users/ken/tmp/stack/.pixi/envs/default/lib/libKGENCompilerRTShared.dylib+0xb9e4)
#2 0x00000001026d4694 stdlib::builtin::error::Error::__init__[__mlir_type.!kgen.string](::StringLiteral[$0])_REMOVED_ARG open-source/max/mojo/stdlib/stdlib/builtin/error.mojo:159:38
#3 0x00000001026d4694 stacktrace_error_capture::func2()_REMOVED_ARG /Users/ken/tmp/stack/stacktrace_error_capture.mojo:14:16
#4 0x00000001026d4694 stacktrace_error_capture::func1() /Users/ken/tmp/stack/stacktrace_error_capture.mojo:18:10
#5 0x00000001026d4694 stacktrace_error_capture::main() /Users/ken/tmp/stack/stacktrace_error_capture.mojo:23:14
#6 0x00000001026d4694 stdlib::builtin::_startup::__wrap_and_execute_raising_main[fn() raises -> None](::SIMD[::DType(int32), ::Int(1)],__mlir_type.!kgen.pointer<pointer<scalar<ui8>>>),main_func="stacktrace_error_capture::main()" open-source/max/mojo/stdlib/stdlib/builtin/_startup.mojo:88:18
#7 0x00000001026d4694 main open-source/max/mojo/stdlib/stdlib/builtin/_startup.mojo:103:4Without enabling stack trace generation, the output is:
Intentional error
--------------------
stack trace was not collected. Enable stack trace collection with environment variable `MOJO_ENABLE_STACK_TRACE_ON_ERROR`Declare a raising function
A function defined using the fn keyword is non-raising by default. So if it
can raise an error, you must include the raises keyword in the function
definition. For example:
fn incr(n: Int) raises -> Int:
if n == Int.MAX:
raise "inc: integer overflow"
else:
return n + 1If you don't include the raises keyword on an fn function,
then the function must explicitly handle any errors that might occur in the code
it executes. For example:
# This function doesn't compile because of the unhandled error
fn unhandled_error(n: Int):
print(n, "+ 1 =", incr(n))
# This function compiles because it handles the possible error
fn handled_error(n: Int):
try:
print(n, "+ 1 =", incr(n))
except e:
print("Handled an error:", e)In contrast, a def function is raising by default. So the following
incr() function is equivalent to the incr() function defined above with
fn:
def incr(n: Int) -> Int:
if n == Int.MAX:
raise "inc: integer overflow"
else:
return n + 1Handle an error
Mojo allows you to detect and handle error conditions using the try-except
control flow structure. The full syntax is:
try:
# Code block to execute that might raise an error
except <optional_variable_name>:
# Code block to execute if an error occurs
else:
# Code block to execute if no error occurs
finally:
# Final code block to execute in all circumstancesYou must include one or both of the except and finally clauses. The else
clause is optional.
The try clause contains a code block to execute that might raise an error. If
no error occurs, the entire code block executes. If an error occurs, execution
of the code block stops at the point that the error is raised. Your program then
continues with the execution of the except clause, if provided, or the
finally clause.
If the except clause is present, its code block executes only if an error
occurred in the try clause. The except clause "consumes" the error that
occurred in the try clause. You can then implement any error handling or
recovery that's appropriate for your application.
If you provide the name of a variable after the except keyword, then the
Error instance is bound to the variable if an error occurs. The Error type
implements the Writable trait, so you can
pass it as an argument to the print()
function if you'd like to print its error message to the console. It also
implements the Stringable trait, so you
can construct a String with String(error) if you want to extract the error
message as a String for further processing.
If desired, you can re-raise an error condition from your except clause simply
by executing a raise statement from within its code block. This can be either
a new Error instance or, if you provided a variable name to capture the
Error that occurred originally, you can re-raise that error.
If the else clause is present, its code block executes only if an error does
not occur in the try clause. Note that the else clause is skipped if the
try clause executes a continue, break, or return that exits from the
try block.
If the finally clause is present, its code block executes after the try
clause and the except or else clause, if applicable. The finally clause
executes even if one of the other code blocks exits by executing a continue,
break, or return statement or by raising an error. The finally clause is
often used to release resources used by the try clause (such as a file handle)
regardless of whether an error occurred.
As an example, consider the following program:
def incr(n: Int) -> Int:
if n == Int.MAX:
raise "inc: integer overflow"
else:
return n + 1
def main():
for value in [0, 1, Int.MAX]:
try:
print()
print("try =>", value)
if value == 1:
continue
result = "{} incremented is {}".format(value, incr(value))
except e:
print("except =>", e)
else:
print("else =>", result)
finally:
print("finally => ====================")Running this program generates the following output:
try => 0
else => 0 incremented is 1
finally => ====================
try => 1
finally => ====================
try => 9223372036854775807
except => inc: integer overflow
finally => ====================Use 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 in the case of error conditions.
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 actually 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 anErrorargument. For more information, see Define a conditional__exit__()method 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, Movable):
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(((end_time - 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 actions:
- 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 type of error condition and propagates all others:
import time
@fieldwise_init
struct ConditionalTimer(ImplicitlyCopyable, Movable):
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(((end_time - 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 badWas this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!