This isn’t weird at all. If you check IsValid function C++, it tells you This does not indicate that there is an active timer that this handle references, but rather that it once referenced a valid timer.
/**
* Returns whether the timer handle is valid. This does not indicate that there is an active timer that this handle references, but rather that it once referenced a valid timer.
* @param Handle The handle of the timer to check validity of.
* @return Whether the timer handle is valid.
*/
UFUNCTION(BlueprintPure, meta=(DisplayName = "Is Valid", ScriptName = "IsValidTimerHandle"), Category="Utilities|Time")
static bool K2_IsValidTimerHandle(FTimerHandle Handle);
What you want to use instead of IsValid is IsTimerActiveByHandle:
Then you’ll get the ff. result in your case:
false false -> no timers are active yet
true false -> timer1 is active and waiting for 3 secs, timer2 is not active yet
false true -> timer1 executed and now inactive, timer2 is active and waiting for 10 secs
false false -> timer1 was already inactive but still invalidated, timer2 executed and still active but then invalidated immediately
Additionally, here’s what it says about IsValidTimerHandle (IsTimerActiveByHandle) in code:
/**
* Returns true if a timer exists and is active for the given handle, false otherwise.
* @param Handle The handle of the timer to check whether it is active.
* @return True if the timer exists and is active.
*/
UFUNCTION(BlueprintPure, meta=(DisplayName = "Is Timer Active by Handle", ScriptName = "IsTimerActiveHandle", WorldContext="WorldContextObject"), Category="Utilities|Time")
static bool K2_IsTimerActiveHandle(UObject* WorldContextObject, FTimerHandle Handle);