4
\$\begingroup\$

From time to time, I encounter a hard-to-reproduce bug with a similar line of code:

## File: Level.gd

func _on_player_invisible() -> void:
    player.queue_free()    

which sometimes crashes with: Attempt to call queue_free in base null instance on a null instance


I have added this null check condition, but the crash can still be observed:

## File: Level.gd

func _on_player_invisible() -> void:
    if player: player.queue_free()    
\$\endgroup\$
0

1 Answer 1

4
\$\begingroup\$

Reproducing the error

I managed to reproduce this reliably. It happens when calling queue_free on a Node that has already been freed (but the reference is not actually null, the error message is misleading).

This code should produce the error:

player.queue_free()
yield(get_tree(), "idle_frame")
player.queue_free()

For the error to happen, it is necessary and sufficient that a frame happens between both calls of queue_free. If multiple calls of queue_free happen in the same frame, there is no error. Multiple frames happening between the calls of queue_free do not prevent the error.


Observed behaviour

Before calling queue_free for the first time, we observe:

print(player == null)                  # false
print(!is_instance_valid(player))      # false
print(player.is_queued_for_deletion()) # false

After calling queue_free, we observe:

print(player == null)                  # false
print(!is_instance_valid(player))      # false
print(player.is_queued_for_deletion()) # true

After a frame passes (and thus the Node has been freed), we observe:

print(player == null)                  # false
print(!is_instance_valid(player))      # true
print(player.is_queued_for_deletion()) # ERROR

The error is: "Attempt to call function 'is_queued_for_deletion' in base 'null instance' on a null instance." (notice that it isn't actually null, instead it has been freed). As a consequence is_queued_for_deletion is not a good filter by itself.

For reference, if we call is_queued_for_deletion on null, we get: "Invalid call. Nonexistent function 'is_queued_for_deletion' in base 'Nil'.".

Finally, if we attempt to call queue_free again, we get the error you saw: "Attempt to call function 'queue_free' in base 'null instance' on a null instance.".


Addendum on Nil vs Null: One of the possible types a Variant can have is Nil, which by definition is null. This is what you get if you write null on GDScript (A Variant of type Nil). However a "null instance" is not of type Nil. Instead you get "null instance" when you have an invalid pointer. Thus, C++ methods which do not return Variant but can return an invalid pointer will not give you Nil (For example get_node_or_null) and if they give you an invalid pointer, and you try to use it you get the "null instance" error. The same error you get with queue_free as explained in this answer (after the Node is freed, any pointers to it are now invalid).


Preventing the error

Therefore to prevent the error when the code can call queue_free multiple times, we want to avoid calling it when a frame has passed since the last call. That is, we want to avoid calling it when is_instance_valid(...) is false. In other words, we only call it when is_instance_valid(...) is true:

if is_instance_valid(player):
    player.queue_free()

Note that is_instance_valid(null) is also false. So we don't need to check for null.

Thus, checking is_instance_valid(...) is both necessary and sufficient to prevent the error. We could check for is_queued_for_deletion also to make sure that queue_free is only ever called once (although this is not necessary to prevent the error).


How queue_free works, looking at the source code

Looking at the source for queue_free, the first thing we find is that it actually binds to the method queue_delete (source). Which looks like this (source):

void SceneTree::queue_delete(Object *p_object) {

    _THREAD_SAFE_METHOD_
    ERR_FAIL_NULL(p_object);
    p_object->_is_queued_for_deletion = true;
    delete_queue.push_back(p_object->get_instance_id());
}

That null check produces a different error (source).

There you can see where _is_queued_for_deletion is set to true, which we check with is_queued_for_deletion().

Note that no check is done on whether or not this is called multiple times.

As per the process that actually frees the Node, it is in _flush_delete_queue (source). It consumes the queue, and calls memdelete (source).

The method memdelete calls the "predelete handler" for the object if it has one, then the destructor and finally releases the memory. It is likely that there is a "predelete handler" as the Object class has one (source) that sends the NOTIFICATION_PREDELETE. Which the Node class handles (source) by setting its owner to null, removing itself from its parent, and then removing all the children and calling memdelete on them.

Finally, it is the destructor of the Object class (source) the one that disconnect signals connect from it, then disconnects the signals connected to it. Then removes the instance from the object database… Which is what is_instance_valid checks.

\$\endgroup\$
2
  • \$\begingroup\$ Is it worth to report an issue and ask for a more relevant error message? \$\endgroup\$
    – pietrodito
    Commented May 17, 2021 at 7:11
  • \$\begingroup\$ @pietrodito I'd go with yes open it, and let the dev decide if it is worth fixing, and how. It appears it remains the same in Godot 4.0 (so far), I haven't found where the error text is in the 3.3 branch (In my defense Github only searches the default branch). Note: You will get this error if using the result of get_node_or_null on a non existing node path. Which is likely how most people will encounter it, and what most search results will point you to if you search for it. \$\endgroup\$
    – Theraot
    Commented May 17, 2021 at 12:17

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .