How do you debug a crash from an invalid UObject reference?
This tracker crashes a few seconds after an enemy dies: UpdateScore dereferences a nullptr or garbage address. The check if (LastKilledEnemy) does not help — the pointer is non-null yet the memory is freed.
// AScoreTracker.h
UCLASS()
class AScoreTracker : public AActor
{
GENERATED_BODY()
public:
void TrackEnemy(AEnemy* Enemy);
void UpdateScore();
private:
AEnemy* LastKilledEnemy = nullptr;
};
// AScoreTracker.cpp
void AScoreTracker::TrackEnemy(AEnemy* Enemy)
{
LastKilledEnemy = Enemy;
}
void AScoreTracker::UpdateScore()
{
int32 Bonus = LastKilledEnemy->GetScoreValue(); // crash
}
Find and fix the bug.
Read the callstack to find the dereferenced pointer and check whether it is a UPROPERTY(). A non-reflected pointer dangling after GC is the usual cause; fix it by adding UPROPERTY() or using TWeakObjectPtr with an IsValid guard.
- ✗Removing
UPROPERTY()to 'stop the GC nulling' — that causes the dangle - ✗Assuming the crash is build-config specific rather than a real lifetime bug
- ✗Forcing
CollectGarbage()instead of fixing the missing reference
- →How would
gc.PendingKillEnabledor stomp allocator help isolate the crash? - →Why does adding
UPROPERTY()change a crash into a safe null dereference you can guard?
Symptom
The game crashes at random moments, often a few seconds after an enemy is destroyed. The callstack points at a nullptr or garbage-address dereference inside a method.
Buggy code
// AScoreTracker.h
UCLASS()
class AScoreTracker : public AActor
{
GENERATED_BODY()
public:
void TrackEnemy(AEnemy* Enemy);
void UpdateScore();
private:
// BUG: raw pointer with no UPROPERTY()
AEnemy* LastKilledEnemy = nullptr;
};
// AScoreTracker.cpp
void AScoreTracker::TrackEnemy(AEnemy* Enemy)
{
LastKilledEnemy = Enemy; // stored a raw pointer
}
void AScoreTracker::UpdateScore()
{
// A few frames later the enemy is destroyed and collected.
// LastKilledEnemy now dangles — the GC never nulled it.
int32 Bonus = LastKilledEnemy->GetScoreValue(); // CRASH
}
Diagnosis
Because LastKilledEnemy is not marked UPROPERTY(), the garbage collector cannot see this reference. When the enemy becomes unreachable and is collected, the GC does not null the pointer — it dangles. An if (LastKilledEnemy) check is useless: the pointer is non-null but the memory is already freed. The crash is delayed, so the callstack points at UpdateScore, not at the real cause.
Fix
private:
// Option A: strong reference — UPROPERTY() keeps the enemy alive
// and nulls the pointer when the enemy is destroyed.
UPROPERTY()
AEnemy* LastKilledEnemy = nullptr;
// Option B: weak reference — does not keep the enemy alive,
// but reports invalidity safely.
TWeakObjectPtr<AEnemy> LastKilledEnemyWeak;
void AScoreTracker::UpdateScore()
{
// With option A: the check now actually works.
if (IsValid(LastKilledEnemy))
{
int32 Bonus = LastKilledEnemy->GetScoreValue();
}
// With option B:
if (AEnemy* Enemy = LastKilledEnemyWeak.Get())
{
int32 Bonus = Enemy->GetScoreValue();
}
}
The choice depends on intent: if the tracker should logically extend the enemy's lifetime, use option A; if it merely observes and the enemy may vanish at any time, use option B.