Skip to main content

Common Mistakes

These are the patterns that cause the most confusion and wasted debugging time for people new to Pulse. Read through them once — you'll recognise most of them when you make them later.


1. Caching _PulseGetHumanoid() across frames

What it looks like:

defineComponent('AutoHeal', () => {
const enabled = signal(false)
const cachedH = _PulseGetHumanoid() // WRONG — stale after respawn

on.heartbeat({ when: enabled }, () => {
if (cachedH) cachedH.Health = cachedH.MaxHealth // errors after death
})
})

Why it's wrong: _PulseGetHumanoid() reads LocalPlayer.Character.Humanoid at the moment it's called. A value cached at component setup time points to the old humanoid, which no longer exists after the player respawns.

What to do instead: Always call the helper fresh inside the handler:

on.heartbeat({ when: enabled }, () => {
const h = _PulseGetHumanoid()
if (!h) return
h.Health = h.MaxHealth
})

2. Not guarding character references

What it looks like:

on.heartbeat({ when: enabled }, () => {
_PulseGetHumanoid()!.WalkSpeed = speed() // crashes when humanoid is nil
})

Why it's wrong: _PulseGetHumanoid() returns undefined when the player is dead, in a vehicle, or hasn't spawned yet. The ! non-null assertion doesn't make it safe — it just silences the type error.

What to do instead:

on.heartbeat({ when: enabled }, () => {
const h = _PulseGetHumanoid()
if (!h) return
h.WalkSpeed = speed()
})

Rule of thumb: every handler that touches humanoid, hrp, or character should check before use.


3. Calling GetChildren() every Heartbeat

What it looks like:

on.heartbeat({ when: enabled }, () => {
const targets = workspace.FindFirstChild('Enemies')!.GetChildren() // WRONG — 60×/sec!
for (const t of targets) {
// process each one
}
})

Why it's wrong: GetChildren() is not free. Calling it 60 times per second for every active feature tanks performance. In games with many instances, this noticeably impacts FPS.

What to do instead: Throttle with every, or cache at a lower frequency:

on.heartbeat({ when: enabled, every: 1.0 }, () => {
const folder = workspace.FindFirstChild('Enemies') as Folder | undefined
const targets = folder ? folder.GetChildren() : []
for (const t of targets) {
// process each one
}
})

Or build a cached list refreshed on a timer and consumed every frame.


4. Forgetting the game load guard

What it looks like:

defineComponent('Targeter', () => {
// runs immediately at inject time — game might not be loaded!
const camera = workspace.CurrentCamera // can be nil
const folder = workspace.FindFirstChild('Enemies') // might not exist yet
})

Why it's wrong: At inject time the game may still be loading. workspace.CurrentCamera is nil. Custom folders may not exist yet.

What to do instead: Wrap load-dependent init in a spawn with a load check:

defineComponent('Targeter', () => {
task.spawn(() => {
if (!game.IsLoaded()) game.Loaded.Wait()
// safe to access workspace, camera, and custom folders
const camera = workspace.CurrentCamera
const folder = workspace.WaitForChild('Enemies', 10)
})
})

5. Yielding inside on.signal handlers

What it looks like:

on.signal(enabled, (v) => {
if (v) {
task.wait(1) // WRONG — blocks synchronous signal propagation
startLoop()
}
})

Why it's wrong: Signal change handlers fire synchronously. task.wait inside them stalls the current thread, which can delay other subscribers receiving the same change and causes unpredictable behaviour.

What to do instead:

on.signal(enabled, (v) => {
if (v) {
task.spawn(() => {
task.wait(1)
startLoop()
})
}
})

Or better, restructure so you don't need the delay at all.


6. Using wait() instead of task.wait()

What it looks like:

on.heartbeat({ when: enabled }, () => {
// @ts-ignore
wait(0.1) // WRONG: legacy API
})

Why it's wrong: Roblox's legacy wait() is inaccurate (minimum ~0.03 s regardless of argument), can throttle under load, and is officially deprecated.

What to do instead:

task.wait(0.1) // accurate, not throttled
task.delay(0.1, () => { ... })
task.spawn(() => { ... })

7. Creating Roblox connections without on.*

What it looks like:

defineComponent('Watcher', () => {
game.GetService('RunService').Heartbeat.Connect(() => {
// does something
})
// connection is never tracked!
})

Why it's wrong: When the script is re-run or destroyed, this connection is never cleaned up. Re-inject ten times and you have ten heartbeat callbacks piling up.

What to do instead: Use on.* hooks — they're tracked and auto-disconnected:

defineComponent('Watcher', () => {
on.heartbeat(() => {
// auto-disconnected when script destroys
})
})

If you truly need a raw connection (e.g. an event on.* doesn't cover), track it with Pulse.Conn.track(connection).


8. Parenting highlights to other players' characters

What it looks like:

const h = new Instance('Highlight')
h.Parent = otherPlayer.Character // WRONG — silently destroyed by replication

Why it's wrong: Roblox's replication system destroys Highlight instances parented to models owned by other players. The highlight appears briefly then vanishes. This is a Roblox constraint, not a Pulse bug.

What to do instead: Parent to CoreGui with an Adornee:

const h = new Instance('Highlight')
h.Adornee = otherPlayer.Character
h.Parent = game.GetService('CoreGui')

9. Naming pages "Home" or "Settings"

What it looks like:

// src/pages/1_Home.ts
definePage('Home', { icon: 'house' }, () => [...]) // WRONG

Why it's wrong: The framework creates "Home" and "Settings" tabs automatically. A user-defined page with either name creates a duplicate tab that may not behave correctly.

What to do instead: Use any other name:

definePage('Combat', { icon: 'swords' }, () => [...])
definePage('Visuals', { icon: 'eye' }, () => [...])
definePage('Misc', { icon: 'settings-2' }, () => [...])

10. Using signals for table state

What it looks like:

const selectedTargets = signal([]) // WRONG — tables not supported

Why it's wrong: Pulse signals only support boolean, number, and string values. A table default will cause unexpected behaviour or a runtime error.

What to do instead: Use a regular let variable inside the component for table state:

defineComponent('Targeter', () => {
let selectedTargets: Model[] = [] // internal, not reactive

on.heartbeat({ every: 1 }, () => {
selectedTargets = getTargets() // refresh on your schedule
})
})

For state that needs to persist or be shared across components, use Pulse.Store:

Pulse.Store.set('selectedTargets', [])
const targets = Pulse.Store.get('selectedTargets') as Model[]