# \`std.DoublyLinkedList\` with nodes created out of scope

**URL:** <https://ziggit.dev/t/std-doublylinkedlist-with-nodes-created-out-of-scope/18186>\
**Category:** Help\
**Tags:** language, standard-library\
**Created:** [November 2, 2026, 2:34pm UTC](https://ziggit.dev/t/std-doublylinkedlist-with-nodes-created-out-of-scope/18186 "2026-11-02T14:34:17Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![devanbenz](https://ziggit.dev/user_avatar/ziggit.dev/devanbenz/32/10433_2.png) [@devanbenz](https://ziggit.dev/u/devanbenz)\
**Post date:** [November 2, 2026, 2:34pm UTC](https://ziggit.dev/t/std-doublylinkedlist-with-nodes-created-out-of-scope/18186/1 "2026-11-02T14:34:17Z")

</div>

With Zig’s `std.DoublyLinkedList` how can I access elements that are inserted outside of the function scope? For example I have the following code where I’m playing around with `std.DoublyLinkedList` while attempting to implement a node for a b+tree I’m building out.

I’ve added code comments to the area of code I’m having troubles with. I think I understand _why_ this doesn’t work, it has to do with scoping. But, what’s a good way to restructure this code so it works as expected 🤔

_Please also let me know if there is anything that’s idiomatic in Zig which I’m not doing, I’m used to Rust and Go so methods on structs are pretty common, not sure if it’s idiomatic zig and I just need to re-structure my code._

```zig
const std = @import("std");

pub const NodeType = enum {
    Root,
    Inner,
    Leaf,
};

fn NodePage(comptime key_type: type, comptime value_type: type) type {
    return struct {
        key: key_type,
        value: value_type,
        node: std.DoublyLinkedList.Node = .{},

        const Self = @This();

        pub const empty: Self = .{
            .key = undefined,
            .value = undefined,
        };
    };
}

var node_list: std.DoublyLinkedList = .{};

pub const Node = struct {
    key_type: type,
    value_type: type,
    allocator: std.mem.Allocator,
    node_type: NodeType,
    node_page: type,

    const Self = @This();

    pub fn init(
        comptime key_type: type,
        comptime value_type: type,
        allocator: std.mem.Allocator,
        node_type: NodeType,
    ) Self {
        var node_page: type = undefined;
        if (std.meta.eql(node_type, NodeType.Root) or std.meta.eql(node_type, NodeType.Inner)) {
            node_page = NodePage(key_type, ?*Node);
        } else {
            node_page = NodePage(key_type, value_type);
        }

        return Self{
            .key_type = key_type,
            .value_type = value_type,
            .allocator = allocator,
            .node_type = node_type,
            .node_page = node_page,
        };
    }

    pub fn deinit(self: Self) void {
        self.allocator.destroy(self.list);
    }

    pub fn insert(self: Self, key: self.key_type, value: self.value_type) void {
        if (node_list.len() == 0) {
            var insertion: NodePage(
                self.key_type,
                self.value_type,
            ) = .{
                .key = key,
                .value = value,
            };

            node_list.append(&insertion.node);
            // The following works, since, insertion was created in this scope
            var it = node_list.first;
            while (it) |node| : (it = node.next) {
                const val = @as(*NodePage(self.key_type, self.value_type), @fieldParentPtr("node", node));
                std.debug.print("{any}\n", .{val.key});
                std.debug.print("{any}\n", .{val.value});
                return;
            }
        }
    }

    pub fn get(self: Self, _: self.key_type) ?self.value_type {
        // What is a good way to make it so this scope can see the pointers from 
        // insertion(s)? Obviously this does not work and I get unitialized garbage data here.
        var it = node_list.first;
        while (it) |node| : (it = node.next) {
            const val = @as(*NodePage(self.key_type, self.value_type), @fieldParentPtr("node", node));
            std.debug.print("{any}\n", .{val.key});
            std.debug.print("{any}\n", .{val.value});
            return null;
        }
        return null;
    }
};

```

---

<div class="post-metadata">

**Author:** ![rdmsr](https://ziggit.dev/user_avatar/ziggit.dev/rdmsr/32/8660_2.png) [@rdmsr](https://ziggit.dev/u/rdmsr)\
**Post date:** [November 2, 2026, 2:36pm UTC](https://ziggit.dev/t/std-doublylinkedlist-with-nodes-created-out-of-scope/18186/2 "2026-11-02T14:36:48Z")

</div>

The problem here is that you’re storing the node on the stack. The typical solution to this is to let the caller handle the node storage and pass it itself to insert. Otherwise, it would also be possible to allocate it non-intrusively on the heap and handle the lifetime accordingly (like you seem to do already in deinit!)

---

<div class="post-metadata">

**Author:** ![devanbenz](https://ziggit.dev/user_avatar/ziggit.dev/devanbenz/32/10433_2.png) [@devanbenz](https://ziggit.dev/u/devanbenz)\
**Post date:** [November 2, 2026, 2:41pm UTC](https://ziggit.dev/t/std-doublylinkedlist-with-nodes-created-out-of-scope/18186/3 "2026-11-02T14:41:27Z")

</div>

Ah okay, so I just need to ensure that I’m allocating the insertions or let the caller handle them? That makes sense. This is supposed to be completely internal to the library so I’ll likely handle them by allocating to the heap.

---

<div class="post-metadata">

**Author:** ![devanbenz](https://ziggit.dev/user_avatar/ziggit.dev/devanbenz/32/10433_2.png) [@devanbenz](https://ziggit.dev/u/devanbenz)\
**Post date:** [November 2, 2026, 2:54pm UTC](https://ziggit.dev/t/std-doublylinkedlist-with-nodes-created-out-of-scope/18186/4 "2026-11-02T14:54:50Z")

</div>

Okay, I would expect something like this to work when managing the lifetimes of the insertion objects:

```zig
    pub fn deinit(self: Self) void {
        var it = node_list.first;
        while (it) |node| : (it = node.next) {
            const val = @as(*NodePage(self.key_type, self.value_type), @fieldParentPtr("node", node));
            self.allocator.destroy(val);
        }
    }

    pub fn insert(self: Self, key: self.key_type, value: self.value_type) !void {
        if (node_list.len() == 0) {
            var insertion = try self.allocator.create(NodePage(self.key_type, self.value_type));
            insertion.* = .{
                .key = key,
                .value = value,
            };

            node_list.append(&insertion.node);
        }
    }

    pub fn get(self: Self, _: self.key_type) ?self.value_type {
        var it = node_list.first;
        while (it) |node| : (it = node.next) {
            const val = @as(*NodePage(self.key_type, self.value_type), @fieldParentPtr("node", node));
            std.debug.print("{any}\n", .{val.key});
            std.debug.print("{any}\n", .{val.value});
        }
        return null;
    }

```

I’m getting a segmentation fault for the deinit traversal

```zig
Segmentation fault at address 0x777172b20008
/home/devan/Documents/Projects/zigtree/src/node.zig:60:39: 0x123a02c in deinit__anon_43295 (root.zig)
        while (it) |node| : (it = node.next) {
                                      ^

```

But when I add a return (keep in mind my list has one item at this point I’ve only inserted one thing) it works fine:

```zig
    pub fn deinit(self: Self) void {
        var it = node_list.first;
        while (it) |node| : (it = node.next) {
            const val = @as(*NodePage(self.key_type, self.value_type), @fieldParentPtr("node", node));
            self.allocator.destroy(val);
            return;
        }
    }

```

Do I need to keep track of how many items the list has? Why does the `while` loop seem to work fine within the `get` method?

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [November 2, 2026, 3:28pm UTC](https://ziggit.dev/t/std-doublylinkedlist-with-nodes-created-out-of-scope/18186/5 "2026-11-02T15:28:26Z")

</div>

You first destroy the node (by destroying the `NodePage` that owns the `std.DoublyLinkedList.Node`) and then you try to use it to access the pointer to the next node, which then accesses memory that is no longer valid.

Instead you can do something like this:

```zig
pub fn deinit(self: Self) void {
    var it = node_list.first;
    while (it) |node| {
        it = node.next;
        const val = @as(*NodePage(self.key_type, self.value_type), @fieldParentPtr("node", node));
        self.allocator.destroy(val);
    }
}

```

To avoid accessing `node` after it has been destroyed by destroying its containing struct.  
The continuation expression runs after the body was evaluated and thus it runs too late, because at that time `node.next` accesses memory that was destroyed / freed.

Adding the return just causes the continuation expression to never be evaluated because you exit the while loop before it would run.

---

<div class="post-metadata">

**Author:** ![devanbenz](https://ziggit.dev/user_avatar/ziggit.dev/devanbenz/32/10433_2.png) [@devanbenz](https://ziggit.dev/u/devanbenz)\
**Post date:** [November 2, 2026, 4:04pm UTC](https://ziggit.dev/t/std-doublylinkedlist-with-nodes-created-out-of-scope/18186/6 "2026-11-02T16:04:51Z")

</div>

> [@Sze](#):
>
> To avoid accessing `node` after it has been destroyed by destroying its containing struct.  
> The continuation expression runs after the body was evaluated and thus it runs too late, because at that time `node.next` accesses memory that was destroyed / freed.

Ah okay I see, thanks! That makes sense.
