The VScript Book

Chapter 4.5: Error Handling & Defensive Programming

In a perfect world, every entity you search for would exist, every function would receive valid parameters, and nothing would ever go wrong. In reality, scripts must be prepared for the unexpected. A missing entity, a null reference, or an invalid operation can crash your script—or worse, crash the entire game.

This chapter teaches you how to write defensive code that anticipates and handles errors gracefully.

The try/catch Block: Catching Errors

Squirrel provides try/catch blocks, which let you attempt potentially dangerous code and recover if it fails.

// Attempt to execute risky code
try {
    local door = Entities.FindByName(null, "exit_door")
    door.SetOrigin(Vector(0, 0, 100)) // If 'door' is null, this will throw an error
}
catch (error) {
    // If an error occurs, this block runs instead of crashing
    printl("ERROR: " + error)
    printl("The door entity was not found or is invalid.")
}

When to Use Try/Catch

try/catch is useful when:

  • You're working with user input or dynamically generated data
  • You're executing code from a string (e.g., compilestring())
  • You want to gracefully handle errors in non-critical systems (like optional cosmetic effects)

Warning: try/catch adds overhead. Don't wrap every line of code in it. Use it strategically for truly unpredictable situations. For most cases, defensive checks (see below) are faster and clearer.

Defensive Programming: Validate Before You Act

The best way to avoid errors is to check your assumptions before performing dangerous operations. This is called defensive programming.

Always Validate Entity Handles

The most common source of errors is trying to use a null or invalid entity handle.

// ❌ UNSAFE: No validation
function TeleportDoor() {
    local door = Entities.FindByName(null, "exit_door")
    door.SetOrigin(Vector(0, 0, 100)) // CRASH if door is null!
}

// ✅ SAFE: Validate first
function TeleportDoorSafe() {
    local door = Entities.FindByName(null, "exit_door")
    
    if (door == null) {
        printl("ERROR: exit_door not found!")
        return // Exit the function early
    }
    
    if (!door.IsValid()) {
        printl("ERROR: exit_door is not valid!")
        return
    }
    
    door.SetOrigin(Vector(0, 0, 100)) // Safe to use now
}

Create Wrapper Functions

To avoid repeating validation logic, create reusable "safe" wrapper functions.

// A safe version of GetOrigin that never crashes
function SafeGetOrigin(entity) {
    if (entity == null || !entity.IsValid()) {
        return Vector(0, 0, 0) // Return a safe default
    }
    return entity.GetOrigin()
}

// Usage
local door = Entities.FindByName(null, "maybe_missing_door")
local doorPos = SafeGetOrigin(door) // Always safe, even if door is null
printl("Door position: " + doorPos)

Practical Example: Robust Entity Finder

Let's combine these techniques into a production-ready helper function:

// A smart entity finder with built-in error handling
function FindEntitySafe(targetname, required = false) {
    local entity = Entities.FindByName(null, targetname)
    
    if (entity == null) {
        local msg = "Entity '" + targetname + "' not found."
        
        if (required) {
            // For required entities, throw an error
            throw(msg)
        } else {
            // For optional entities, just log a warning
            printl("WARNING: " + msg)
        }
    }
    
    return entity
}

// Usage
local criticalDoor = FindEntitySafe("exit_door", true)  // Will throw error if missing
local optionalLight = FindEntitySafe("fancy_light", false) // Will log warning if missing

if (optionalLight) {
    EntFireByHandle(optionalLight, "TurnOn", "", 0, null, null)
}