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 allocate and release resources such as files correctly, 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")
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"
raise "integer overflow"
An error interrupts the current execution flow of your program. If you provide an error handler (as described in Handle an error) in the current scope or any calling scope, execution resumes with that handler. If there is no applicable error handler, your program terminates with a non-zero exit code and prints the error message. For example:
Unhandled exception caught during execution: integer overflow
Unhandled exception caught during execution: integer overflow
If a function you define using the fn
keyword 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 + 1
fn incr(n: Int) raises -> Int:
if n == Int.MAX:
raise "inc: integer overflow"
else:
return n + 1
On the other hand, you cannot use the raises
keyword when defining a
function using the def
keyword, because def
already implies that the
function may raise an error. So the following is equivalent to the function
defined above:
def incr(n: Int) -> Int:
if n == Int.MAX:
raise "inc: integer overflow"
else:
return n + 1
def incr(n: Int) -> Int:
if n == Int.MAX:
raise "inc: integer overflow"
else:
return n + 1
Handle an error
Mojo allows you to detect and handle error conditions using the try-except
control flow structure, whose 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 circumstances
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 circumstances
You 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 pass it to the str()
function 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 exit 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 or not 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():
values = List(0, 1, Int.MAX)
for value in values:
try:
print()
print("try =>", value[])
if value[] == 1:
continue
result = str("{} incremented is {}").format(value[], incr(value[]))
except e:
print("except =>", e)
else:
print("else =>", result)
finally:
print("finally => ====================")
def incr(n: Int) -> Int:
if n == Int.MAX:
raise "inc: integer overflow"
else:
return n + 1
def main():
values = List(0, 1, Int.MAX)
for value in values:
try:
print()
print("try =>", value[])
if value[] == 1:
continue
result = str("{} 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 => ====================
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()
# 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 result in the program not executing the close()
.
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()
# 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 in conjunction 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 that 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 needed
with open(input_file, "r") as f:
content = f.read()
# Process the content as needed
The 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)
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 thewith
statement 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 thewith
code block completes execution, even if thewith
code 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
with
code block raises an error, then the__exit__()
method runs before any error processing occurs (that is, before it is caught by atry-except
structure or your program terminates). If you'd like to define conditional processing for error conditions in awith
code block, you can implement an overloaded version of__exit__()
that takes anError
argument. 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 thewith
code 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
@value
struct Timer:
var start_time: Int
fn __init__(inout self):
self.start_time = 0
fn __enter__(inout self) -> Self:
self.start_time = time.perf_counter_ns()
return self
fn __exit__(inout 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)
if len(sys.argv()) > 1:
raise "simulated error"
time.sleep(1)
print("Ending execution")
import sys
import time
@value
struct Timer:
var start_time: Int
fn __init__(inout self):
self.start_time = 0
fn __enter__(inout self) -> Self:
self.start_time = time.perf_counter_ns()
return self
fn __exit__(inout 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)
if len(sys.argv()) > 1:
raise "simulated error"
time.sleep(1)
print("Ending execution")
Running this example produces output like this:
mojo context_mgr.mojo
mojo context_mgr.mojo
Beginning execution
Ending execution
Elapsed time: 2010.0 milliseconds
Beginning execution
Ending execution
Elapsed time: 2010.0 milliseconds
mojo context_mgr.mojo fail
mojo context_mgr.mojo fail
Beginning execution
Elapsed time: 1002.0 milliseconds
Unhandled exception caught during execution: simulated error
Beginning execution
Elapsed time: 1002.0 milliseconds
Unhandled exception caught during execution: simulated error
Define 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 -> Bool
fn __exit__(self, error: Error) raises -> Bool
Given the Error
that occurred as an argument, the method can:
- Return
True
to suppress the error - Return
False
to 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 sys
import time
@value
struct ConditionalTimer:
var start_time: Int
fn __init__(inout self):
self.start_time = 0
fn __enter__(inout self) -> Self:
self.start_time = time.perf_counter_ns()
return self
fn __exit__(inout 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__(inout self, e: Error) raises -> Bool:
if str(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")
import sys
import time
@value
struct ConditionalTimer:
var start_time: Int
fn __init__(inout self):
self.start_time = 0
fn __enter__(inout self) -> Self:
self.start_time = time.perf_counter_ns()
return self
fn __exit__(inout 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__(inout self, e: Error) raises -> Bool:
if str(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 bad
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 bad
Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!