Queue_free question

Godot Version

4.4.1

Question

Hello!

When I call queue_free() on an object, it becomes a “deleted” one.
This object evaluates as false in boolean checks, which is fine.

However, when this “deleted object” is passed to a strongly typed function, the program crashes due to a type mismatch. That’s not fine :sweat_smile:
This only seems to happen when the variable is global to the script.

Here’s a simple scene with a Node3D and a Timer:

extends Node3D
@onready var timer: Timer = $Timer

func some_func(node: Node3D):
	print(node)

var node: Node3D

func _ready():
	node = Node3D.new()
	some_func(node)
	node.queue_free()
	timer.timeout.connect(func(): some_func(node))
	timer.start(1)

This raises the error:
“Invalid type in function ‘some_func’ in base ‘Node3D (test_queue_free.gd)’. The Object-derived class of argument 1 (previously freed) is not a subclass of the expected argument class.”

However, doing the same thing with a local variable works as expected (IMHO):

extends Node3D
@onready var timer: Timer = $Timer

func some_func(node: Node3D):
	print(node)

func _ready():
	var node: Node3D
	node = Node3D.new()
	some_func(node)
	node.queue_free()
	timer.timeout.connect(func(): some_func(node))
	timer.start(1)

At first I thought the local variable just became null when it went out of scope, but no—because if I remove queue_free(), the node is still valid inside the timeout() callback.

This workaround avoids the crash:

timer.timeout.connect(func(): some_func(node if node else null))

But… it’s kinda weird to have to write this everywhere, right? :sweat_smile:

Am I missing something here?
Is there a more elegant or “Godot-style” way to deal with this?

Usually you avoid this kind of issue by not doing anything with queue free’d nodes.

Seems odd that you are trying to run code on a node that you are deleting?

1 Like

What if you disconnect after deleting the node?

Queue_free should be the last thing you do. To put code after that is bad and quite honestly a bit daft to even try. A node is not deleted immediately, but at the end of the frame, but the error you are getting is because you are trying to connect to an item that is now queued to be freed. Obviously that is a bad idea.

What if you disconnect after deleting the node?

What do you mean? Do you mean in a multiplayer scenario? Or with Godot web editor?

This only seems to happen when the variable is global to the script.

Again I am not sure what you mean here as I cannot see any global references in your example, just an anonymous function trying to connect to an already freed node. A freed node, even pre-deletion, is no longer a valid instance of an object any more.

I can’t see any valid reason to attempt to put code either in a node after a queue_free command, or code that tries to refer to the freed node immediately after freeing it.

PS If you have a global script that is not sure if a node still exists or not, checking that it is still a valid reference is perfectly logical too. It is no different than checking a variable is not zero before using it as a divisor.

1 Like

I meant something like

timer.timeout.disconnect(some_func)

but after thinking for a moment, that function should keep working for the case in which the node returns to be valid, so one should reconnect again in that case… this seems a bit cumbersome and prone to bug

Please, don’t take this code as “the code in use”. It’s a simple code to test the issue.

Actually, in the real code there are tests if the object is valid to be used because it may be deleted or be null. This is a “last attacked npc” variable, to be precise. So, when NPC is defeated, it’s deleted from tree.
I recoded the game so now the GameManager object is the responsable of removing npcs from scene and emit a “npc_about_to_delete” signal. Then, every player connect to this signal for removing the “last attacked npc” variable reference. It’s working fine now.

The point is that if the functions are not strongly typed won’t be any problem. Neither if the object reference is in a function-scoped variable instead of script. What if I used a local variable getting the reference of a global variable that will be deleted at anytime?
See that the error raises when passing variable in functions, not by using them (in that case, of course, it will raise a correct “trying to use a deleted/null variable” error)
I’ve never seen this kind of error. For the literal error text, seems that Godot changes the object type at runtime. Thats odd, at least, for me.

Look at the variable declaration of “node”

This version raises the error:

extends Node3D
@onready var timer: Timer = $Timer

func some_func( node:Node3D ) :
	print(node)

# SCRIPT GLOBAL
var node:Node3D
func _ready() :
	node = Node3D.new()
	some_func(node)
	node.queue_free()
	timer.timeout.connect( func() : some_func(node) )
	timer.start(1)

But this one doesn’t:

extends Node3D
@onready var timer: Timer = $Timer

func some_func( node:Node3D ) :
	print(node)

func _ready() :
# LOCAL VARIABLE
	var node = Node3D.new()
	some_func(node)
	node.queue_free()
	timer.timeout.connect( func() : some_func(node) )
	timer.start(1)

Note that timer.connect may be used before queue_free as node is “queued” and not actually freed until next frame. The result are the same.
I can go even further: using some_func.call_deferred(node) works fine and “node” is still valid: not yet freed

1 Like

@flaxRabbit
My apologies, I assumed you were the OP asking a follow up question. My bad!

1 Like

I am pretty sure this is something to do with how references are handled between lambda’s taking there own copy of a reference and globals being a direct reference, but to be honest I cannot work out why there is the difference either.

Either way, both these versions are buggy, probably both should crash, and neither is a good idea at best.

PS I hope someone can explain as I am interested in the answer!

2 Likes

You could perhaps make some_func() take a Variant argument instead, and test the argument’s class/state inside before using it.

1 Like

Yeah. I also tried and works.

You’re saying to do something like this:

extends Node3D
@onready var timer: Timer = $Timer

func some_func( node ) :
	print(node if node else null)

var node:Node3D
func _ready() :
	node = Node3D.new()
	some_func(node)
	node.queue_free()
	timer.timeout.connect( func() : some_func(node) )
	timer.start(1)

Actually, the “some_func” real function in the code does that because the parameter may be null. It’s good practice to check parameter validity at the very begining in a public function class body.

I prefer to use typed variables/parameters because the IDE will know class funcs and variables for intellisense
Anyway, something is telling me that I’m missing something, is a bug…

Looks like it is, indeed, a bug and this pull fixes it.

2 Likes

Yes, it’s not exactly what I’m facing, but this enlights what’s happening.

I’m absolutely agree with the last comment: “as” and “is” should never throw errors.

On variable assignements (also in parameter assignement for functions) the way to go is performing a implicit casting (an “as”, in Godot scripting)
So the returned value for this casting should be “null” instead of raising runtime error for freed objects.
Another solution should be that freed objects remember their original class type. So, no odd tricks.
But nullifying them, I guess is quite fine.

I don’t know how other languages with garbage collector behave in this situations.

1 Like