Should GetGameplayAbilityFromSpecHandle return null instead of the CDO when no instance exists?

We’re hitting an ensure on CurrentActorInfo in GetAvatarActorFromActorInfo during client join/rejoin in our multiplayer game (listen server host). The root cause is a replication timing window during FastArrayDeltaSerialize of FGameplayAbilitySpecContainer.

When a client joins, all ability specs are added to the ActivatableAbilities array, and then PostReplicatedAdd fires sequentially for each spec. During PostReplicatedAdd for spec A, our game code broadcasts a delegate from OnGiveAbility. A UI widget handles that delegate and calls GetGameplayAbilityFromSpecHandle for a different ability (spec B) whose spec is already in the array but whose PostReplicatedAdd hasn’t fired yet, so no instance has been created.

GetGameplayAbilityFromSpecHandle in AbilitySystemBlueprintLibrary.cpp calls GetPrimaryInstance, gets null, and falls back to returning AbilitySpec->Ability (the CDO) with bIsInstance set to false.

[Image Removed]

We’ve fixed the Blueprint to check bIsInstance, but the broader question is about the CDO fallback itself.

Given that non-instanced abilities are deprecated (multiple spots in GameplayAbility.cpp explicitly says so and ensures against it), is there still a valid use case for GetGameplayAbilityFromSpecHandle returning the CDO? For InstancedPerActor and InstancedPerExecution abilities, calling any function that touches CurrentActorInfo, CurrentActivationInfo, or other instance state on the CDO will always be wrong.

Would it be appropriate to change the fallback to return null?

if (!AbilityInstance)
{
    // AbilityInstance = AbilitySpec->Ability;  // <- Fix - Remove this line
    bIsInstance = false;
}

This would make the function return null for instanced abilities that don’t have an instance yet, which is consistent with how the other early-return paths already work (invalid ASC and invalid handle both return null). Callers already need to handle null from those paths.

[Attachment Removed]

Hello! Just letting you know I reached out to the dev that originally wrote these functions for comment. I’ll update you when I know more.

[Attachment Removed]

After speaking to the dev: the CDO fallback return value was not written with networking race conditions in mind. My thoughts: returning null is probably clearer so that game code can more easily recognize that there is a network race condition, rather than operating on the CDO.

That said:

  • Since the behavior has been like this in released engine versions already, we don’t want to change it right now. Changing this is low priority for us. If we would change it, it would be cvar-ed.
  • I would keep your workaround of detecting the CDO at the call-site, or return null with an engine modification.

Be aware that FindAllAbilitiesWithTags and FindAllAbilitiesMatchingQuery have the same fallback behavior.

[Attachment Removed]