> For the complete Mojo documentation index, see [llms.txt](/llms.txt).
> Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).

# 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**](#raise-an-error) — Use the built-in `Error` type to
  raise errors with string messages.
- [**Handle an error**](#handle-an-error) — Use `try`/`except`/`else`/`finally`
  to detect and recover from errors.
- [**Typed errors**](#typed-errors) — Define custom error types as structs for
  structured error data and compile-time type checking.
- [**Representing multiple error conditions**](#representing-multiple-error-conditions)
  — Use enumerated error types or the `Variant` type for pattern matching.
- [**The `Never` type**](#the-never-type) — Mark functions that always raise
  or never raise.
- [**Parametric raises**](#parametric-raises) — Write generic functions that
  propagate error types from their arguments.
- [**Typed and `Error` interaction**](#typed-errors-and-error-interaction) —
  Work with code that uses both error styles.
- [**Stack traces**](#enable-stack-trace-generation-for-errors) — Enable stack
  trace collection for debugging.
- [**Context managers**](#use-a-context-manager) — 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`](#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 handler, your program terminates with a non-zero exit code
and prints the error message:

```output
Unhandled exception caught during execution: record not found
```

## Raise an error

The built-in [`Error`](/docs/std/builtin/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:

```mojo
# 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:

```mojo
def read_file_fn(path: String) raises -> String:
    if not path:
        raise "path cannot be empty"
    return "contents of " + path
```

:::tip

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](#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:

```mojo
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 *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:

```mojo title="handle_error.mojo"
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)
```

```output

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](/docs/manual/values/ownership/#transfer-arguments-var-and-)
(`^`) to transfer ownership of the error value:

```mojo
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](/docs/manual/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`](/docs/std/format/Writable/) trait is recommended so the error
produces a readable message when printed or when the program terminates with an
unhandled error:

```mojo
@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 [`@fieldwise_init`](/docs/reference/decorators/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")`.

:::note

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](/docs/manual/gpu/fundamentals/).

:::

### Raise a typed error

To declare that a function can raise a typed error, add `raises YourErrorType`
to its signature:

```mojo
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:

```mojo
# 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:

```mojo
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:

```output
Error in field 'username': cannot be empty
```

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:

```mojo
# 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)
```

:::note

If your error type implements the [`Writable`](/docs/std/format/Writable/)
trait, you can also pass `e` directly to `print()`:

```mojo
except e:
    print(e)  # calls ValidationError.write_to()
```

Which produces this output:

```output
ValidationError(username): cannot be empty
```

:::

## 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:

- **Enumerated error types** — A single struct with `comptime` variant aliases.
  Simpler and more efficient when you only need to distinguish between
  conditions.
- **The `Variant` type** — The standard library
  [`Variant`](/docs/std/utils/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](/docs/manual/metaprogramming/comptime-evaluation/#comptime-values)
as named constants. The `write_to` method generates human-readable strings from
the variant code:

```mojo
@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
[`Equatable`](/docs/std/builtin/comparable/Equatable/), the compiler
auto-synthesizes `__eq__()`, so you can compare variants directly:

```mojo
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:

```mojo
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:

```output
Permission denied: FileError.permission_denied
```

### The `Variant` type

When each error condition needs to carry different data, you can use the
standard library [`Variant`](/docs/std/utils/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:

```mojo title="variant_errors.mojo"
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:

```mojo
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:

```mojo
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])
```

```output
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](#enumerated-error-types) 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 like `panic()` that
  unconditionally signal an error.
- `raises Never -> ReturnType` — The function *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:

```mojo
# 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:

```mojo
# 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](#parametric-raises), where the compiler infers
`raises Never` when a function argument doesn't raise.

## Parametric raises

You can write
[generic functions](/docs/manual/parameters/#parameters-and-generics) that
propagate the error type from a function argument to the caller. This uses a
compile-time parameter for the error type:

```mojo
def run_action[
    ErrorType: AnyType
](https://mojolang.org/docs/manual/action: def(.md) 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`:

```mojo
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:

```mojo
def 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:

```output
Got value: 99
```

## 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:

```mojo
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 *type erasure*—the compiler forgets the specific error type,
even though the runtime preserves the error's identity:

```mojo
# 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`:

```mojo
try:
    _ = validate_bare_raises(-5)
except e:
    # e is typed as Error — no field access available
    # e.field would not compile here
    print(e)
```

```output
ValidationError(value): cannot be negative
```

The error message still shows `ValidationError` because the runtime preserves
the original error's [`Writable`](/docs/std/format/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 *uncaught* errors that propagate through a
bare `raises` function. If you catch the typed error locally, you still get full
field access:

```mojo
def error_caller():
    try:
        _ = validate_typed(-5)
    except e:
        # e is a ValidationError — field access works
        print("Field:", e.field, "Reason:", e.reason)
```

```output
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:

```mojo
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)
```

The compiler reports:

```output
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](#wrap-error-at-api-boundaries).

### Recommendations for mixed codebases

When working with both `Error` and typed errors:

- **Use `raises YourErrorType`** — Always specify the error type in function
  signatures. Bare `raises` discards type information.
- **Use separate `try` blocks** — When calling functions with different error
  types, use nested or sequential `try` blocks to handle each type
  independently.

:::note

For a complete working example of these interaction patterns, see
[`error_interaction.mojo`](https://github.com/modular/modular/blob/main/mojo/docs/code/manual/errors/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.

:::important

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 *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
[`mojo build`](/docs/cli/build/), the compiler optimizes and strips symbols by
default, so often your stack trace won't be very useful.

Consider this program:

```mojo title="stacktrace_error.mojo"
def func2() raises -> None:
    raise Error("Intentional error")

def func1() raises -> None:
    func2()

def main() raises:
    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:

```sh
mojo build stacktrace_error.mojo
```

```sh
MODULAR_DEBUG=stack-trace-on-error ./stacktrace_error
```

```output
#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:

```sh
mojo build --debug-level full stacktrace_error.mojo
```

```sh
MODULAR_DEBUG=stack-trace-on-error ./stacktrace_error
```

```output
#0 0x... llvm::sys::PrintStackTrace(llvm::raw_ostream&, int)
#1 0x... KGEN_CompilerRT_GetStackTrace
#2 0x... Error.__init__[...](https://mojolang.org/docs/manual/...) .../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[...](https://mojolang.org/docs/manual/...) .../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__()`.

:::note

Running your program directly with [`mojo run`](/docs/cli/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
[`get_stack_trace()`](/docs/std/builtin/error/Error/#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:

```mojo title="stacktrace_error_capture.mojo"
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")
```

When you compile with debug symbols and run with stack trace generation enabled:

```sh
mojo build --debug-level full stacktrace_error_capture.mojo
```

```sh
MODULAR_DEBUG=stack-trace-on-error ./stacktrace_error_capture
```

```output
Intentional error
--------------------
#0 0x... llvm::sys::PrintStackTrace(llvm::raw_ostream&, int)
#1 0x... KGEN_CompilerRT_GetStackTrace
#2 0x... Error.__init__[...](https://mojolang.org/docs/manual/...) .../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[...](https://mojolang.org/docs/manual/...) .../builtin/_startup.mojo:88:18
#7 0x... main .../builtin/_startup.mojo:103:4
```

Without enabling stack trace generation, the output is:

```output
Intentional error
--------------------
No stack trace available
```

## 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 when errors occur.

:::note

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:

```mojo
# 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()`](/docs/std/io/file/FileHandle/#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()`](/docs/std/io/file/FileHandle/#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:

```mojo
# 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`](/docs/std/io/file/FileHandle/) struct
returned by [`open()`](/docs/std/io/file/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:

```mojo
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:

```mojo
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`](/docs/std/tempfile/tempfile/NamedTemporaryFile/),
[`TemporaryDirectory`](/docs/std/tempfile/tempfile/TemporaryDirectory/),
[`BlockingScopedLock`](/docs/std/utils/lock/BlockingScopedLock/), and
[`assert_raises`](/docs/std/testing/testing/assert_raises/). You can also
create your own custom context managers, as described in [Write a custom context
manager](#write-a-custom-context-manager) below.

## Write a custom context manager

Writing a custom context manager is a matter of defining a
[struct](/docs/manual/structs/) that implements two special *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](#define-a-conditional-__exit__-method)
  and
  [Handle typed errors in `__exit__()`](#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:

```mojo title="context_mgr.mojo"
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:

```sh
mojo context_mgr.mojo
```

```output
Beginning execution
Ending execution
Elapsed time: 2010.0 milliseconds
```

```sh
mojo context_mgr.mojo fail
```

```output
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:

```mojo
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:

```mojo title="conditional_context_mgr.mojo"
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")
```

Running this example produces this output:

```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
```

### 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:

```mojo
def __exit__[ErrType: AnyType](https://mojolang.org/docs/manual/self, err: ErrType.md) -> Bool
```

This method receives the typed error directly, preserving its full type
information. You can use
[reflection](/docs/manual/metaprogramming/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](https://mojolang.org/docs/manual/err.md)` — Access the error through its `Writable`
  interface.

The following `ResourceGuard` example demonstrates this pattern:

```mojo
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](https://mojolang.org/docs/manual/self, err: ErrType.md) -> 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](https://mojolang.org/docs/manual/err.md))

        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:

```mojo
# 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")
```

```output
Acquiring: database
Working...
Releasing: database (no error)

Acquiring: cache
Releasing: cache
  Error type: ConnectionError
  Message: ConnectionError: connection timed out
Continued after suppressed error
```

:::note

For a complete working example including error suppression and propagation, see
[`resource_guard.mojo`](https://github.com/modular/modular/blob/main/mojo/docs/code/manual/errors/resource_guard.mojo).

:::
