How to Debug a Rust Oxide Plugin: Compile Errors, Exceptions and Slow Hooks
To debug an Oxide plugin, read the server console and the files in oxide/logs: compile errors appear as "Error while compiling <Name>" with numbered messages and line:column positions, runtime errors appear as "Failed to call hook" with a stack trace. Add targeted Puts or PrintWarning lines, reload with oxide.reload, and test on a local server with a second account. Most crashes in hooks are null references, and slow hooks are reported automatically once they pass 100 ms.
10 min readUpdated Oct 26, 2026
To debug an Oxide plugin, watch the server console while you save the file, read the first error message, fix it, and reload. Oxide reports three kinds of problems: compile errors (the plugin never loads), runtime exceptions (the plugin loads but a hook, command or timer fails), and performance warnings (a hook runs too long). Each has a distinct log format, covered below. The same workflow applies on Carbon, which loads the same plugin files.
Set up a loop that makes errors visible
Debugging is fastest when you see the result of every edit within seconds.
- Run a local Rust dedicated server with Oxide (or Carbon) installed. See Rust plugin development setup and install Oxide on a Rust server.
- Keep the server console visible in a window next to your editor.
- Save the
.csfile intooxide/plugins. Oxide detects the change and reloads the plugin automatically. - If you want to force it, run
oxide.reload PluginNamefrom the console.oxide.loadandoxide.unloadwork the same way. More commands are in Oxide console commands.
After each save, a successful load prints Loaded plugin <Title> v<Version> by <Author>. There is no "compiled successfully" line, so that message is your confirmation. If you do not see it, the plugin did not load; scroll up for an error.
Use oxide.plugins to list what is loaded. A plugin missing from that list failed to compile or failed to initialize.
Reading compile errors
A compile failure looks like this:
Error while compiling PluginName:
[1] The name 'playr' does not exist in the current context at 42:17
[2] ; expected at 57:30The format is Error while compiling <PluginName>: followed by numbered Roslyn messages, each ending with at <line>:<column>. The line and column refer to your file, counted from 1.
How to work through them:
- Fix the first error first. One missing brace or semicolon can produce many follow-up messages further down. Fix
[1], save, and see what remains. - Go to the exact position. Open the file at the given line and column. For a missing semicolon, the reported line is often the one after the actual problem, so check the previous line too.
- Look at the CS code. The message text is shown in the console, and the CS number is in the compiler log (see next section). Searching the CS code finds the cause quickly.
A failed file is skipped, and the other plugins still compile and load. Nothing else on the server is affected, except plugins that depend on the broken one.
Frequent compile errors
| Symptom | Likely cause | Page |
|---|---|---|
| name does not exist | typo, missing variable, wrong scope | CS0103 |
| type or namespace not found | missing using line | CS0246 |
[JsonProperty] not found | missing using Newtonsoft.Json; | CS0246 JsonProperty |
| does not contain a definition | wrong member name, or a member removed by a game update | CS1061 |
Random is ambiguous | bare Random used | CS0104 |
| semicolon expected | missing ; | CS1002 |
| closing brace expected | unbalanced braces | CS1513 |
| not all code paths return a value | a hook or method with a missing return | CS0161 |
| cannot convert ulong to string | passing userID where a string id is expected | CS1503 |
Some errors are not about syntax at all. If Oxide says it cannot find the main class, or the file name does not match, see plugin filename must match main class and unable to find main plugin class. If the Info attribute is the problem, see Info attribute missing.
If a plugin that worked yesterday now fails with does not contain a definition, a Rust update probably changed the game code. See fix Rust plugins after an update.
The oxide/logs folder
The console scrolls quickly, especially on a busy server. Oxide writes its output to files in oxide/logs (Carbon uses carbon/logs). The compiler errors, including the CS code for each message, are recorded there, so open the newest log when the console has already moved on or when you need the exact error code.
Practical habits:
- Open the log file right after a failed reload rather than scrolling the console.
- Search the log for the plugin name to see every error that belongs to it.
- On a live server, check the logs after each wipe-day update before players join.
Runtime exceptions and stack traces
A plugin that compiles can still fail while running. Oxide catches the exception, logs it, and keeps the server alive. The message depends on where it happened:
| Where | Message |
|---|---|
| Inside a hook | Failed to call hook '<Hook>' on plugin '<Name> v<Version>' (<Type>: <Message>) |
| In Init or Loaded | Failed to initialize plugin ... |
| In a chat command | Failed to run command '/<cmd>' on plugin '<Name>'. (...) |
| In a timer | Failed to run a <seconds> timer in '<Name> v<Version>' |
A stack trace follows. Read it like this:
Failed to call hook 'OnEntityTakeDamage' on plugin 'Example v1.0.0' (NullReferenceException: Object reference not set to an instance of an object)
at Oxide.Plugins.Example.OnEntityTakeDamage (BaseCombatEntity entity, HitInfo info) [0x00010] in <...>:0- The text in brackets after the hook name is the exception type and message. This tells you what went wrong.
- The first frame that names your plugin class tells you where. Frames from the game or Oxide below it are the callers.
- The hook name tells you which game event triggered it. Look up its arguments on the hook page, for example OnEntityTakeDamage.
Common exception types have their own pages:
- NullReferenceException in a hook
- InvalidCastException in a hook
- KeyNotFoundException
- Collection was modified during enumeration
- IndexOutOfRangeException in a command
- Failed to run timer
- Failed to initialize plugin
A timer that throws is destroyed, so a repeating task silently stops after its first failure. If a periodic feature "just stops working", look for a timer error earlier in the log. More about timers in Rust plugin timers.
Tracing with Puts and PrintWarning
When the stack trace does not explain the bug, log the values involved. Puts writes a normal line to the console and log; PrintWarning is more visible when you scan output.
private void OnPlayerRespawned(BasePlayer player)
{
if (player == null) return;
PrintWarning($"[debug] respawned {player.displayName} at {player.transform.position}");
}Rules that keep tracing useful:
- Prefix messages with something searchable such as
[debug], and remove them when done. - Print the values you suspect: whether a reference is null, a count, an id, a config value.
- Do not leave logging inside frequently fired hooks such as OnEntityTakeDamage, OnDispenserGather, OnEntitySpawned, OnPlayerInput or OnTick. They fire many times per second and will flood the console and slow the server. If you must trace one, log only when a condition is met, such as a specific player or prefab.
- Use a debug switch in your config so you can turn tracing on without recompiling.
private void Trace(string message)
{
if (config.Debug) Puts("[debug] " + message);
}Here config.Debug is a bool in your config class; see Rust plugin config file.
Check that the hook runs at all
If nothing happens, first prove the hook fires. Add one line at the top of the method. If it never prints, the cause is one of these:
- The method name or parameter types do not match the hook signature. Oxide finds methods by hook name and calls only overloads whose parameter types accept the arguments. A parameter declared more specifically than what the game passes (for example
BasePlayerwhere the hook sometimes passes another entity type) makes Oxide skip those calls. See Oxide hooks explained. - The hook was removed or renamed in a game update. Check monthly hook updates and deprecated hook warnings.
- Another plugin returned a non-null value earlier and cancelled the action. On many hooks any non-null value, true or false, cancels the default behavior, so look for conflicting plugins.
- The plugin is not loaded. Run
oxide.plugins.
If the hook runs but the result is wrong, check what you return. For hooks that cancel on any non-null value, return null to keep default behavior. Do not assume true forces the action; check each hook's page.
Common NullReferenceException sources
Most runtime failures are null references. Check these first:
- HitInfo and its members.
info,info.InitiatorPlayer,info.Initiatorandinfo.HitEntitycan be null, for example with fall or decay damage. - Players that are not real players.
GetOwnerPlayer()on an item returns null for items in a box. NPCs and sleepers can reach hooks that you assumed were players only. - Connections.
player.netorplayer.net.connectionmay be null for NPCs or during disconnect. - Item data.
item.infoanditem.parentcan be null on items being created or moved. - Entities after destruction. A saved or cached entity may be destroyed; check
entity == null || entity.IsDestroyed. - Config and data. A config field missing from an old file, or a data object that was never created, is null until you initialize it.
- Other plugins. A
[PluginReference]field is null when that plugin is not loaded. UseEconomics?.Call(...). See calling other plugins. - Unity objects. Components found via
GetComponentmay be absent.
A defensive pattern:
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
{
if (entity == null || info == null) return null;
var attacker = info.InitiatorPlayer;
if (attacker == null || attacker.IsNpc) return null;
// safe to use attacker and entity here
return null;
}Also watch for modifying a list while looping over it, which causes the "collection was modified" exception. Loop over a copy or collect items first, then change them.
Measure slow hooks
Oxide logs a warning when a hook call takes longer than 100 ms:
Calling 'OnEntityTakeDamage' on 'Example v1.0.0' took 134msIf a garbage collection happened during the call, the message ends with [GARBAGE COLLECT]. That points at heavy allocation rather than slow logic. See took ms hook performance warning and took average ms garbage collect.
To find the cause:
- Look for loops over
BaseNetworkable.serverEntitiesorBasePlayer.activePlayerListinside frequent hooks. Cache results or run them on a timer instead. - Avoid building strings, lists or lambdas on every call of a hot hook.
- To measure a block yourself, record
UnityEngine.Time.realtimeSinceStartupbefore and after and print the difference only if it exceeds a threshold.
float start = UnityEngine.Time.realtimeSinceStartup;
DoExpensiveWork();
float ms = (UnityEngine.Time.realtimeSinceStartup - start) * 1000f;
if (ms > 20f) PrintWarning($"DoExpensiveWork took {ms:0.0} ms");More techniques are in Rust plugin performance.
Test with a second account
Many bugs only show up with two players: damage between players, team and clan logic, permissions, loot locks, teleport requests, and anything with "owner" or "other player".
- Join with your main account and a second Steam account or a second game client if you have one. A friend can also join your local server.
- Grant permissions to one account only, so you can compare behavior with and without them:
oxide.grant user <name|id> pluginname.use. Check withoxide.show user <name>. See Oxide permissions and groups. - For admin-only effects, remember that admins can bypass some checks, so test the normal-player path with a non-admin account.
- Test disconnect and reconnect too, using OnPlayerDisconnected and OnPlayerConnected, and test with sleepers present.
Check lifecycle problems
Several bugs come from running code at the wrong time. The order is Init, config and lang loading, Loaded, then OnServerInitialized. On a running server the plugin loads after startup, and OnServerInitialized is called immediately with false. Plugins that touch entities or players in Init can fail on a fresh boot because the world is not ready. Move that work to OnServerInitialized.
Also confirm that Unload cleans up. A leftover timer or UI after oxide.reload makes behavior look doubled. Destroy timers and CUI in Unload. See Rust CUI basics.
Other startup messages worth recognizing: duplicate permission registered, missing plugin name prefix for permission, and failed to load config file. A corrupt config makes the plugin fall back to defaults only if you coded that fallback; see the config guide.
A short debugging checklist
- Did it load? Look for
Loaded plugin, or runoxide.plugins. - If not, read
Error while compilingand fix error[1]first. - If it loaded, trigger the action and watch for
Failed to call hookorFailed to run command. - Find the first stack frame in your class and check each member on that line for null.
- Add a temporary trace line to confirm the hook fires and the values are what you expect.
- Test as a non-admin with and without permission, with a second account.
- Watch for
took ...mswarnings after the fix.
If you want a clean starting point that already contains null checks, config, lang and permission handling, the plugin generator produces one. For writing from scratch, see how to make a Rust plugin.
FAQ
Why does my plugin not show up in oxide.plugins after I saved it?
Most likely it failed to compile and was skipped. Check the console and oxide/logs for "Error while compiling". Other causes are a file name that differs from the main class name, or a missing Info attribute. Fix the cause, save the file again, and Oxide will retry loading it automatically.
Does a compile error in one plugin stop the other plugins from loading?
No. A file that fails to compile is skipped and the remaining plugins still compile and load. If another plugin depends on the broken one, that dependent plugin may log a missing dependency problem, so fix the failing file first and then check the rest.
What happens to a timer that throws an exception?
Oxide logs "Failed to run a <seconds> timer in '<Name> v<Version>'" with the exception details, and the timer is destroyed. A repeating timer therefore stops silently after its first failure, so wrap risky work in null checks and re-create the timer if needed.
Can I find which line of my plugin threw the exception?
Yes. The stack trace below the "Failed to call hook" line lists the method names in your plugin, usually with line numbers when available. The top-most frame inside your plugin is the first place to look. Then check every member access on that line for a possible null.
Does Carbon debug differently from Oxide?
The workflow is the same because Carbon loads Oxide plugins and accepts the oxide.* console commands. The difference is the folder layout: Carbon uses carbon/plugins and carbon/logs instead of the oxide folders, and its message wording may differ slightly from Oxide's.
More guides
Skip the C#: describe your plugin
Open generator