#set document(title: "2.7 Errors, error handling, and context managers", author: "Modular Inc. / XYZ Homework") #set page(width: 8.5in, height: auto, margin: 1in) #import "@preview/cetz:0.5.2" #set text(font: ("STIX Two Text", "Libertinus Serif", "New Computer Modern"), size: 10.5pt, lang: "en") #show math.equation: set text(font: ("STIX Two Math", "New Computer Modern Math")) #set par(justify: true, leading: 0.62em, spacing: 0.9em) #set enum(spacing: 1.1em) // room between list items so tall inline fractions don't collide #set list(spacing: 1.1em) #set table(stroke: 0.5pt + rgb("#c7ccd3")) #let BLUE = rgb("#183B6F") // brand navy — section bars + example/solution labels (white on navy 11.09:1) #let ORANGE = rgb("#A94509") // brand primary-700 — AA-safe deep orange for TEXT (5.93:1 on white; raw brand #F37021 is 2.94:1 and must never carry text) #let RED = rgb("#DC2626") // brand error-600 #let GREEN = rgb("#059669") // brand success-600 (decoration only; small green text uses green-text #007942) #show heading.where(level: 1): it => block(width: 100%, above: 0pt, below: 16pt, fill: gradient.linear(BLUE, rgb("#2C5AA0")), inset: (x: 14pt, y: 12pt), radius: 3pt, text(fill: white, weight: "bold", size: 19pt, it.body)) #show heading.where(level: 2): it => block(width: 100%, above: 18pt, below: 10pt, fill: BLUE, inset: (x: 10pt, y: 6pt), radius: 2pt, text(fill: white, weight: "bold", size: 12pt, it.body)) #show heading.where(level: 3): it => text(fill: ORANGE, weight: "bold", size: 12.5pt, it.body) #show heading.where(level: 4): it => text(fill: BLUE, weight: "bold", size: 10.5pt, it.body) #let examplebox(label, title, body) = block(width: 100%, breakable: true, fill: rgb("#EFF1F5"), stroke: 0.5pt + rgb("#CFDDF0"), radius: 4pt, inset: 10pt, above: 12pt, below: 12pt)[ #block(below: 6pt)[#box(fill: BLUE, inset: (x: 6pt, y: 2pt), radius: 2pt, text(fill: white, weight: "bold", size: 8.5pt, label)) #h(0.4em) #strong[#title]] #body] // rail = decorative left rule (raw brand token); labelcolor = AA-safe label text shade #let notebox(label, rail, labelcolor, tint, body) = block(width: 100%, breakable: true, fill: tint, stroke: (left: 3pt + rail), inset: (left: 10pt, rest: 8pt), radius: (right: 4pt), above: 11pt, below: 11pt)[ #text(fill: labelcolor, weight: "bold", size: 7.5pt, tracking: 0.5pt)[#upper(label)] #linebreak() #body] #let solutionbox(body) = block(above: 4pt, below: 8pt)[ #text(fill: BLUE, weight: "bold", size: 8.5pt)[Solution] #linebreak() #body] #let figph(msg) = block(width: 100%, height: 60pt, fill: rgb("#f6f7f9"), stroke: (paint: rgb("#c7ccd3"), dash: "dashed"), radius: 4pt, inset: 10pt)[ #align(center + horizon, text(fill: rgb("#889"), style: "italic", size: 9pt, msg))] // Standardize inlined figure sizes: measure the natural CeTZ canvas, then scale to a // consistent envelope (aspect-aware; see build_typst.py FIG_* constants). Unlike the // print preamble, dimensions are FLOORED: in an editor a user can trim a figure to a // degenerate 1-D shape (a bare line), and w/h or tw/w would then divide by zero. #let _STD_W = 3.5 #let _WIDE_W = 5.6 #let _MAX_H = 3.4 #let _ASPECT_WIDE = 2.2 #let _UPSCALE_MAX = 1.15 #let stdfig(body) = context { let m = measure(body) let w = calc.max(m.width / 1in, 0.01) let h = calc.max(m.height / 1in, 0.01) let tw = if w / h > _ASPECT_WIDE { _WIDE_W } else { _STD_W } let s = calc.min(tw / w, _MAX_H / h, _UPSCALE_MAX) align(center, box(scale(x: s * 100%, y: s * 100%, reflow: true, body))) } #show figure: set block(breakable: false) #set figure(gap: 8pt) #show figure.caption: set text(size: 8.5pt, fill: rgb("#555")) == 2.7#h(0.6em)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: - #strong[Raise an error] — Use the built-in Error type to raise errors with string messages. - #strong[Handle an error] — Use try/except/else/finally to detect and recover from errors. - #strong[Typed errors] — Define custom error types as structs for structured error data and compile-time type checking. - #strong[Representing multiple error conditions] — Use enumerated error types or the Variant type for pattern matching. - #strong[The Never type] — Mark functions that always raise or never raise. - #strong[Parametric raises] — Write generic functions that propagate error types from their arguments. - #strong[Typed and Error interaction] — Work with code that uses both error styles. - #strong[Stack traces] — Enable stack trace collection for debugging. - #strong[Context managers] — Manage resources safely with the with statement. 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 found === Raise an error The built-in #link("https://mojolang.org/docs/std/builtin/error/Error/")[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. By declaring raises, you tell Mojo that a function may raise an error: def read\_file\_fn(path: String) raises -\> String: if not path: raise "path cannot be empty" return "contents of " + path #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ If you need structured error data—like separate fields for an error code and description—or allocation-free errors for GPU kernels, see Typed errors later on this page. For most application code, Error with a descriptive string message is all you need. ] === Handle 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 outcome You 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 the raise point and continues with the except clause (if present) or the finally clause. - except — Runs only when an error occurs in the try block. If you provide a variable name (except e:), the error is bound to that variable. A try block can have only one except clause. - else — Runs only when no error occurs in the try block. The else clause is #emph[skipped] if the try clause exits via continue, break, or return. - finally — Runs after the try and any except or else clause, regardless of outcome. It executes even if another clause exits via continue, break, return, or by raising a new error. Use finally to 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: def 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() raises: 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-negative Notice: - When id is 5: process\_record() succeeds, so else runs, then finally. - When id is 0: continue exits the try block, skipping both except and else. Only finally runs. - When id is 1001: process\_record() raises an error. The except clause handles it and execution continues with the next iteration. - When id is -3: process\_record() raises an "invalid" error. The except clause re-raises it with raise e^, which transfers ownership of the error to the outer try/except. The finally clause still runs before the error propagates. Because the re-raise exits the loop, id 42 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 transfer You 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 #link("https://mojolang.org/docs/std/format/Writable/")[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 def write\_to(self, mut writer: Some\[Writer\]): writer.write("ValidationError(", self.field, "): ", self.reason) The #link("https://mojolang.org/docs/reference/decorators/fieldwise-init/")[\@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"). #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Typed errors work on GPUs and embedded targets as long as you avoid heap-allocated types like String. For more on GPU programming, see GPU fundamentals. ] ==== Raise a typed error To declare that a function can raise a typed error, add raises YourErrorType to its signature: def validate\_username(username: String) raises ValidationError -\> String: if username.byte\_length() == 0: raise ValidationError(field="username", reason="cannot be empty") if username.count\_codepoints() \< 3: raise ValidationError( field="username", reason="must be at least 3 characters" ) return username Mojo functions are non-raising by default. Including raises (with or without a type) makes it a raising function. Each 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 function calls a raising function, it must handle the error locally: \# This doesn't compile — validate\_username() can raise def process\_name(name: String): print(validate\_username(name)) \# This compiles — the error is handled def 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) Error in field 'username': cannot be empty Which produces this output: A 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) #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ If your error type implements the #link("https://mojolang.org/docs/std/format/Writable/")[Writable] trait, you can also pass e directly to print(): except e: print(e) \# calls ValidationError.write\_to() ValidationError(username): cannot be empty Which produces this output: ] === Representing multiple error conditions Each 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: - #strong[Enumerated error types] — A single struct with comptime variant aliases. Simpler and more efficient when you only need to distinguish between conditions. - #strong[The Variant type] — The standard library #link("https://mojolang.org/docs/std/utils/variant/Variant/")[Variant] type 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) def 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" def write\_to(self, mut writer: Some\[Writer\]): writer.write("FileError.", self.variant\_name()) Because FileError has a single Int field and conforms to #link("https://mojolang.org/docs/std/builtin/comparable/Equatable/")[Equatable], the compiler auto-synthesizes \_\_eq\_\_(), so you can compare variants directly: def open\_file(path: String) raises FileError -\> String: if not path: raise FileError.not\_found if path == "/secret": raise FileError.permission\_denied return "Contents of " + path You 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) Permission denied: FileError.permission\_denied Which produces this output: ==== The Variant type When each error condition needs to carry different data, you can use the standard library #link("https://mojolang.org/docs/std/utils/variant/Variant/")[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 std.utils import Variant \@fieldwise\_init struct NotFoundError(Copyable, Writable): var path: String def 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 def 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: def open\_file(path: String) raises FileError -\> String: if not path: raise FileError(NotFoundError("")) if path == "/secret": raise FileError(PermissionError("/secret", "admin")) return "Contents of " + path In 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 #emph[always] raises and never returns a value. This is useful for functions like panic() that unconditionally signal an error. - raises Never -\> ReturnType — The function #emph[never] raises and always returns a value. This is equivalent to omitting raises entirely. ==== 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 def panic(msg: String) raises -\> Never: raise Error(msg) def 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: def safe\_add(a: Int, b: Int) raises Never -\> Int: return a + b def safe\_add(a: Int, b: Int) -\> Int: return a + b This 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: def run\_action\[ ErrorType: AnyType \](action: def() thin raises ErrorType -\> Int) raises ErrorType -\> Int: return action() The function type uses thin because action is a noncapturing function value. 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: def fetch\_data() raises NetworkError -\> Int: raise NetworkError(code=404) def 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: def get\_value() -\> Int: return 99 \# ... \# ErrorType inferred as Never — no try block needed var result = run\_action(get\_value) print("Got value:", result) Got value: 99 Which produces this output: === Typed errors and Error interaction Most codebases contain a mix of functions that raise, don't raise, or raise typed errors. This section covers how the styles interact and how to work with them effectively. ==== Wrap Error at API boundaries When calling raising 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) raises -\> Int: if value \< 0: raise "value cannot be negative" return value def wrapped\_validate(value: Int) raises ValidationError -\> Int: try: return validate\_with\_error(value) except e: raise ValidationError(field="value", reason=String(e)) ==== Avoid bare raises with typed errors Using bare raises (without a type) on an function that calls typed-error functions causes #emph[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 def 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 negative The error message still shows ValidationError because the runtime preserves the original error's #link("https://mojolang.org/docs/std/format/Writable/")[Writable] output. But the compiler sees only Error, so you lose access to structured fields. Always use raises YourErrorType to maintain type safety. Note that type erasure only affects #emph[uncaught] errors that propagate through a bare raises function. If you catch the typed error locally, you still get 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) Field: value Reason: cannot be negative ==== 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() raises -\> Int: raise "something went wrong" def typed\_func() raises ValidationError -\> Int: raise ValidationError(field="x", reason="invalid") \# This doesn't compile def mixed() raises ValidationError: try: \_ = error\_func() \# raises Error \_ = typed\_func() \# raises ValidationError except e: print(e) error: cannot call function that may raise 'Error' in a context that supports an error type of 'ValidationError' The compiler reports: 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: - #strong[Use raises YourErrorType] — Always specify the error type in function signatures. Bare raises discards type information. - #strong[Use separate try blocks] — When calling functions with different error types, use nested or sequential try blocks to handle each type independently. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ For a complete working example of these interaction patterns, see #link("https://github.com/modular/modular/blob/mojo/v1.0.0b2/mojo/docs/code/manual/errors/error_interaction.mojo")[error\_interaction.mojo]. ] === 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. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Stack traces are a feature of the built-in Error type only. Typed errors currently don't capture stack traces because the trace is collected inside Error.\_\_init\_\_(), and custom error structs have no equivalent hook. The examples in this section all use Error intentionally. ] Mojo generates a stack trace when your program hits a segmentation fault. However, by default Mojo #emph[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 MODULAR\_DEBUG environment variable to stack-trace-on-error, as shown in the examples below. Keep in mind that when you compile your program with #link("https://mojolang.org/docs/cli/build/")[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() raises -\> None: raise Error("Intentional error") def func1() raises -\> None: func2() def main() raises: func1() /Users/mbpro/FromOldMac/Claude/mojo-cas/.pixi/envs/default/bin/mojo: error: execution exited with a non-zero result: 1 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.mojo MODULAR\_DEBUG=stack-trace-on-error ./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 error To 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.mojo MODULAR\_DEBUG=stack-trace-on-error ./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 error With debug symbols, the trace shows the function call chain and source locations: main() → func1() → func2() → Error.\_\_init\_\_(). #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Running your program directly with #link("https://mojolang.org/docs/cli/run/")[mojo run] or mojo doesn't include debug symbols in the stack trace, even with --debug-level full. Use mojo build with -g and run the compiled binary for symbolicated stack traces. ] ==== Capture a stack trace programmatically You can bind the Error instance to a variable in the except clause and call its #link("https://mojolang.org/docs/std/builtin/error/Error/#get_stack_trace")[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() raises -\> None: raise Error("Intentional error") def func1() raises -\> None: func2() def main() raises: 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") Intentional error -------------------- 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.mojo MODULAR\_DEBUG=stack-trace-on-error ./stacktrace\_error\_capture Intentional 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:4 Without enabling stack trace generation, the output is: Intentional error -------------------- No stack trace available === Use a context manager A #emph[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. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Context managers work with both typed errors and the built-in Error type. The with statement handles either error style transparently. ] 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 #link("https://mojolang.org/docs/std/io/file/FileHandle/#close")[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 #link("https://mojolang.org/docs/std/io/file/FileHandle/#read")[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 #link("https://mojolang.org/docs/std/io/file/FileHandle/")[FileHandle] struct returned by #link("https://mojolang.org/docs/std/io/file/open/")[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 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) FileHandle is perhaps the most commonly used context manager. Other examples of context managers in the Mojo standard library are #link("https://mojolang.org/docs/std/tempfile/tempfile/NamedTemporaryFile/")[NamedTemporaryFile], #link("https://mojolang.org/docs/std/tempfile/tempfile/TemporaryDirectory/")[TemporaryDirectory], #link("https://mojolang.org/docs/std/utils/lock/BlockingScopedLock/")[BlockingScopedLock], and #link("https://mojolang.org/docs/std/testing/testing/assert_raises/")[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 #emph[dunder] methods ("double underscore" methods): \_\_enter\_\_() and \_\_exit\_\_(): - \_\_enter\_\_() is called by the with 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 the with code block completes execution, even if the with code block terminates with a call to continue, break, or return. 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 a try/except structure or your program terminates). If you'd like to define conditional processing for error conditions in a with code 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 the with 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 std.sys import std.time \@fieldwise\_init struct Timer(ImplicitlyCopyable): var start\_time: Int def \_\_init\_\_(out self): self.start\_time = 0 def \_\_enter\_\_(mut self) -\> Self: self.start\_time = Int(time.perf\_counter\_ns()) return self def \_\_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() raises: 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.mojo Beginning execution Ending execution Elapsed time: 2010.0 milliseconds mojo context\_mgr.mojo fail 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: def \_\_exit\_\_(self, error: Error) raises -\> Bool Given the Error that occurred as an argument, the method can do any of the following: - 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 error condition and propagates all others: import std.time \@fieldwise\_init struct ConditionalTimer(ImplicitlyCopyable): var start\_time: Int def \_\_init\_\_(out self): self.start\_time = 0 def \_\_enter\_\_(mut self) -\> Self: self.start\_time = Int(time.perf\_counter\_ns()) return self def \_\_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 \_\_exit\_\_(mut self, e: Error) -\> 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) raises -\> Int: if (n % 4) == 0: raise "really bad" elif (n % 2) == 0: raise "just a warning" else: return n def main() raises: 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") 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 Running this example produces this output: ==== Handle 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: def \_\_exit\_\_\[ErrType: AnyType\](self, err: ErrType) -\> Bool This 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: - reflect\[ErrType\].name() — Get the error type's name as a string. - comptime if conforms\_to(ErrType, Writable) — Check if the error implements Writable. - trait\_downcast\[Writable\](err) — Access the error through its Writable interface. The following ResourceGuard example demonstrates this pattern: from std.reflection import \* \@fieldwise\_init struct ConnectionError(Copyable, Writable): var message: String def write\_to(self, mut writer: Some\[Writer\]): writer.write("ConnectionError: ", self.message) struct ResourceGuard(ImplicitlyCopyable): var name: String var suppress\_errors: Bool def \_\_init\_\_(out self, name: String, suppress\_errors: Bool = False): self.name = name self.suppress\_errors = suppress\_errors def \_\_enter\_\_(self) -\> Self: print("Acquiring:", self.name) return self def \_\_exit\_\_(self): print("Releasing:", self.name, "(no error)") def \_\_exit\_\_\[ErrType: AnyType\](self, err: ErrType) -\> Bool: comptime type\_name = reflect\[ErrType\].name() print("Releasing:", self.name) print(" Error type:", type\_name) comptime if conforms\_to(ErrType, Writable): print(" Message:", trait\_downcast\[Writable\](err)) return self.suppress\_errors When 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 error #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ For a complete working example including error suppression and propagation, see #link("https://github.com/modular/modular/blob/mojo/v1.0.0b2/mojo/docs/code/manual/errors/resource_guard.mojo")[resource\_guard.mojo]. ]