Skip to main content

Death of a value

As soon as a value/object is no longer used, Mojo destroys it. Mojo does not wait until the end of a code block—or even until the end of an expression—to destroy an unused value. It destroys values using an “as soon as possible” (ASAP) destruction policy that runs after every sub-expression. Even within an expression like a+b+c+d, Mojo destroys the intermediate values as soon as they're no longer needed.

Mojo uses static compiler analysis to find the point where a value is last used. Then, Mojo immediately ends the value's lifetime and calls the __del__() destructor to perform any necessary cleanup for the type.

For example, notice when the __del__() destructor is called for each instance of Balloon:

@fieldwise_init
struct Balloon(Writable):
    var color: String

    fn write_to[W: Writer](self, mut writer: W) -> None:
        writer.write(String("a ", self.color, " balloon"))

    fn __del__(deinit self):
        print("Destroyed", String(self))


def main():
    var a = Balloon("red")
    var b = Balloon("blue")
    print(a)
    # a.__del__() runs here for "red" Balloon

    a = Balloon("green")
    # a.__del__() runs immediately because "green" Balloon is never used

    print(b)
    # b.__del__() runs here
a red balloon
Destroyed a red balloon
Destroyed a green balloon
a blue balloon
Destroyed a blue balloon

Notice that each initialization of a value is matched with a call to the destructor, and a is actually destroyed multiple times—once for each time it receives a new value.

Also notice that this __del__() implementation doesn't actually do anything. Most structs don't require a custom destructor, and Mojo automatically adds a no-op destructor if you don't define one.

The __del__() method takes its argument using the deinit argument convention, which indicates that the value is being deinitialized.

Default destruction behavior

You may be wondering how Mojo can destroy a type without a custom destructor, or why a no-op destructor is useful. If a type is simply a collection of fields, like the Balloon example, Mojo only needs to destroy the fields: Balloon doesn't dynamically allocate memory or use any long-lived resources (like file handles). There's no special action to take when a Balloon value is destroyed.

When a Balloon value is destroyed, the String value in its color field is no longer used, and it is also immediately destroyed.

The String value is a little more complicated. Mojo strings are mutable. The String object has an internal buffer—a List field, which holds the characters that make up the string. A List stores its contents in dynamically allocated memory on the heap, so the string can grow or shrink. The string itself doesn't have any special destructor logic, but when Mojo destroys a string, it calls the destructor for the List field, which de-allocates the memory.

Since String doesn't require any custom destructor logic, it has a no-op destructor: literally, a __del__() method that doesn't do anything. This may seem pointless, but it means that Mojo can call the destructor on any value when its lifetime ends. This makes it easier to write generic containers and algorithms.

Benefits of ASAP destruction

Similar to other languages, Mojo follows the principle that objects/values acquire resources in a constructor (__init__()) and release resources in a destructor (__del__()). However, Mojo's ASAP destruction has some advantages over scope-based destruction (such as the C++ RAII pattern, which waits until the end of the code scope to destroy values):

  • Destroying values immediately at last-use composes nicely with the "move" optimization, which transforms a "copy+del" pair into a "move" operation.

  • Destroying values at end-of-scope in C++ is problematic for some common patterns like tail recursion, because the destructor call happens after the tail call. This can be a significant performance and memory problem for certain functional programming patterns, which is not a problem in Mojo, because the destructor call always happens before the tail call.

The Mojo destruction policy is more similar to how Rust and Swift work, because they both have strong value ownership tracking and provide memory safety. One difference is that Rust and Swift require the use of a dynamic "drop flag"—they maintain hidden shadow variables to keep track of the state of your values to provide safety. These are often optimized away, but the Mojo approach eliminates this overhead entirely, making the generated code faster and avoiding ambiguity.

Destructor

Mojo calls a value's destructor (__del__() method) when the value's lifetime ends (typically the point at which the value is last used). As we mentioned earlier, Mojo provides a default, no-op destructor for all types, so in most cases you don't need to define the __del__() method.

You should define the __del__() method to perform any kind of cleanup the type requires. Usually, that includes freeing memory for any fields where you dynamically allocated memory (for example, via UnsafePointer) and closing any long-lived resources such as file handles.

However, any struct that is just a simple collection of other types does not need to implement the destructor.

For example, consider this simple struct:

@fieldwise_init
struct Balloons:
    var color: String
    var count: Int

There's no need to define the __del__() destructor for this, because it's a simple collection of other types (String and Int), and it doesn't dynamically allocate memory.

Whereas, the following struct must define the __del__() method to free the memory allocated by its UnsafePointer:

struct HeapArray(Writable):
    var data: UnsafePointer[Int]
    var size: Int

    fn __init__(out self, *values: Int):
        self.size = len(values)
        self.data = UnsafePointer[Int].alloc(self.size)
        for i in range(self.size):
            (self.data + i).init_pointee_copy(values[i])

    fn write_to[W: Writer](self, mut writer: W) -> None:
        writer.write("[")
        for i in range(self.size):
            writer.write(self.data[i])
            if i < self.size - 1:
                writer.write(", ")
        writer.write("]")

    fn __del__(deinit self):
        print("Destroying", self.size, "elements")
        for i in range(self.size):
            (self.data + i).destroy_pointee()
        self.data.free()


def main():
    var a = HeapArray(10, 1, 3, 9)
    print(a)
[10, 1, 3, 9]
Destroying 4 elements

The destructor takes its self argument using the deinit argument convention, which grants exclusive ownership of the value and marks it as destroyed at the end of the function. (For more information on the deinit convention, see Field lifetimes during destruct and move).

Note that a pointer doesn't own any values in the memory it points to, so when a pointer is destroyed, Mojo doesn't call the destructors on those values.

So in the HeapArray example above, calling free() on the pointer releases the memory, but doesn't call the destructors on the stored values. To invoke the destructors, use the destroy_pointee() method provided by the UnsafePointer type.

It's important to notice that the __del__() method is an "extra" cleanup event, and your implementation does not override any default destruction behaviors. For example, Mojo still destroys all the fields in Balloons even if you add a __del__() method that to do nothing:

@fieldwise_init
struct Balloons:
    var color: String
    var count: Int

    fn __del__(deinit self):
        # Mojo destroys all the fields when they're last used
        pass

However, the self value inside the __del__() destructor is still whole (so all fields are still usable) until the destructor returns, as we'll discuss more in the following section.

Field lifetimes

In addition to tracking the lifetime of all objects in a program, Mojo also tracks each field of a structure independently. That is, Mojo keeps track of whether a "whole object" is fully or partially initialized/destroyed, and it destroys each field independently with its ASAP destruction policy.

For example, consider this code that changes the value of a field:

@fieldwise_init
struct Balloons:
    var color: String
    var count: Int


def main():
    var balloons = Balloons("red", 5)
    print(balloons.color)
    # balloons.color.__del__() runs here, because this instance is
    # no longer used; it's replaced below

    balloons.color = "blue"  # Overwrite balloons.color
    print(balloons.color)
    # balloons.__del__() runs here

The balloons.color field is destroyed after the first print(), because Mojo knows that it will be overwritten below. You can also see this behavior when using the transfer sigil:

fn consume(var arg: String):
    pass


fn use(arg: Balloons):
    print(arg.count, arg.color, "balloons.")


fn consume_and_use():
    var balloons = Balloons("blue", 8)
    consume(balloons.color^)
    # String.__moveinit__() runs here, which invalidates balloons.color
    # Now balloons is only partially initialized

    # use(balloons)  # This fails because balloons.color is uninitialized

    balloons.color = String("orange")  # All together now
    use(balloons)  # This is ok
    # balloons.__del__() runs here (and only if the object is whole)

Notice that the code transfers ownership of the name field to consume(). For a period of time after that, the name field is uninitialized. Then name is reinitialized before it is passed to the use() function. If you try calling use() before name is re-initialized, Mojo rejects the code with an uninitialized field error.

Also, if you don't re-initialize the name by the end of the pet lifetime, the compiler complains because it's unable to destroy a partially initialized object.

Mojo's policy here is powerful and intentionally straight-forward: fields can be temporarily transferred, but the "whole object" must be constructed with the aggregate type's initializer and destroyed with the aggregate destructor. This means it's impossible to create an object by initializing only its fields, and it's likewise impossible to destroy an object by destroying only its fields.

Field lifetimes during destruct and move

Both the consuming move constructor and the destructor take their operand with the deinit argument convention. This grants exclusive ownership of the value and marks it as destroyed at the end of the function. Within the function body, Mojo’s ASAP policy still applies to fields: each field is destroyed immediately after its last use.

Just to recap, the move constructor and destructor method signatures look like this:

struct TwoStrings:
    fn __moveinit__(out self, deinit existing: Self):
        # Initializes a new `self` by consuming the contents of `existing`
    fn __del__(deinit self):
        # Destroys all resources in `self`

Like the var argument convention, the deinit convention gives the argument exclusive ownership of a value. But unlike a var argument, Mojo doesn't insert a destructor call for the argument at the end of the function—because the deinit convention already marks it as a value that's in the process of being destroyed.

For example, the following code shows how fields are destroyed inside a destructor.

fn consume(var str: String):
    print("Consumed", str)

@fieldwise_init
struct TwoStrings(Copyable, Movable):
    var str1: String
    var str2: String

    fn __del__(deinit self):
        # self value is whole at the beginning of the function
        self.dump()
        # After dump(): str2 is never used again, so str2.__del__() runs now

        consume(self.str1^)
        # self.str1 has been transferred so str1 becomes uninitialized, and
        # no destructor is called for str1.
        # self.__del__() is not called (avoiding an infinite loop).

    fn dump(mut self):
        print("str1:", self.str1)
        print("str2:", self.str2)


def main():
    var two_strings = TwoStrings("foo", "bar")

Explicit lifetime extension

Most of the time, Mojo’s ASAP destruction “just works.” Very rarely, you may need to explicitly mark the last use of a value to control when its destructor runs. Think of this as an explicit last-use marker for the lifetime checker, not a general-purpose pattern.

You might do this:

  • When writing tests that intentionally create otherwise-unused values (to avoid warnings or dead-code elimination).

  • When writing unsafe/advanced code (for example, code that manipulates a value’s origin).

  • When you need deterministic destructor timing relative to specific side effects (such as logging or profiling).

Mark the last use by assigning the value to the _ discard pattern at the point where it is okay to destroy it. This sets the last use at that line, so the destructor runs immediately after the statement:

# Without explicit extension: s is last used in the print() call, so it is
# destroyed immediately afterwards.
var s = "abc"
print(s)  # s.__del__() runs after this line

# With explicit extension: push last-use to the discard line.
var t = "xyz"
print(t)

# ... some time later
_ = t  # t.__del__() runs after this line

Was this page helpful?