Error Handling and Logging
Every server plugin will encounter errors. A player provides invalid command input, a database connection drops mid-query, a null reference slips through a code path that was not tested. How your plugin handles these errors determines whether the server stays running and whether the operator can diagnose the issue.
RocketMod provides a logging API through the Rocket.Core.Logging.Logger class and defines two exception types in the Rocket.API namespace for common command error scenarios. This article covers error handling patterns, the RocketMod exceptions, logging best practices, and log file management for production servers.
Prerequisites
- Articles 1 through 7 (Fundamentals track), especially article 5 (Commands and IRocketCommand) for command-level error handling.
- Article 31 (Scheduling Tasks and Background Work) for error handling in scheduled tasks.
- Familiarity with C# exception handling -- try-catch-finally, using blocks, and exception types.
What you'll learn
- The two RocketMod exception types and when each is appropriate.
- How to use
Logger.Log(),Logger.LogException(),Logger.LogWarning(), andLogger.LogError()at the appropriate severity levels. - Command-level error handling patterns that give useful feedback to the player and the server operator.
- How to handle exceptions in event handlers, scheduled tasks, and async operations.
- How to implement structured error reporting with context data.
- Log file management -- rotation, size limits, and integration with external log aggregation tools.
RocketMod exception types
RocketMod defines two exception types in the Rocket.API namespace, both tied to command execution.
WrongUsageOfCommandException
Thrown when a player uses a command with incorrect syntax. This exception is caught by RocketMod's command handler, which sends the command's usage message to the caller:
csharp
using Rocket.API;
public void Execute(IRocketPlayer caller, string[] command)
{
if (command.Length < 2)
{
throw new WrongUsageOfCommandException(caller, this);
}
}The exception class is Rocket.API.WrongUsageOfCommandException. Make sure you use the correct class name when throwing this exception in your plugin code:
csharp
// Correct
throw new WrongUsageOfCommandException(caller, this);
// Incorrect -- no such class exists
// WrongCommandUsageExceptionNoPermissionsForCommandException
Thrown automatically by RocketMod's command handler when a player executes a command without the required permission. If the command's Permissions list is not empty and the caller lacks any of the listed permissions, RocketMod throws this exception before your Execute method runs.
You do not need to throw this exception yourself. RocketMod handles it for you. If you need to check permissions manually within your plugin code, use the permissions system instead of re-throwing this exception.
Logger API
RocketMod's Logger class in Rocket.Core.Logging provides static methods for writing log entries. Every method is static; you do not instantiate a Logger object.
Logger.Log()
The primary logging method. Writes a message to the RocketMod log file (Rocket.log) and the server console. Use this for general operational messages:
csharp
using Rocket.Core.Logging;
Logger.Log("Plugin loaded successfully.");
Logger.Log($"Player {player.DisplayName} used command /heal.");Logger.LogWarning()
Writes a warning-level message. Warnings indicate something is wrong but not critical -- the plugin continues operating:
csharp
Logger.LogWarning("Configuration file is missing. Using defaults.");
Logger.LogWarning($"Player {player.DisplayName} triggered an unexpected state in the purchase flow.");Logger.LogError()
Writes an error-level message. Use this for non-exception errors where you have a descriptive message but no exception object:
csharp
Logger.LogError("Failed to connect to database after 3 attempts.");
Logger.LogError($"Invalid state detected in plugin {Name}.");Logger.LogException()
Writes an exception with its full stack trace to the log. This is the primary method for recording caught exceptions:
csharp
try
{
// Risky operation
}
catch (Exception ex)
{
Logger.LogException(ex);
}The LogException() method logs the exception message, type, and full stack trace to the RocketMod log file. This information is essential for diagnosing the root cause of runtime errors. Unlike Logger.Log(), which only records the message you provide, Logger.LogException() captures the complete exception data including inner exceptions.
The log data is available in both debug and release builds.
Logger.ExternalLog()
Writes a log entry intended for external log consumers. Use this when you want to forward structured data to external monitoring tools while also writing to the standard RocketMod log:
csharp
Logger.ExternalLog("Economy transaction: player=12345 amount=500 result=success");ELogType
RocketMod defines the ELogType enum in Rocket.Core.Logging for internal log entry categorization. This enum is used by RocketMod's logging infrastructure. Your plugin code does not typically need to reference ELogType directly -- use the named methods (Log, LogWarning, LogError, LogException) instead.
Try-catch patterns for plugin code
Command-level error handling
Wrap command execution in a try-catch to prevent unhandled exceptions from propagating to the RocketMod command handler:
csharp
public void Execute(IRocketPlayer caller, string[] command)
{
try
{
// Command logic
if (command.Length < 1)
{
UnturnedChat.Say(caller, "Usage: /heal [player]", UnityEngine.Color.red);
return;
}
UnturnedPlayer target = command.Length > 1
? UnturnedPlayer.FromName(command[1])
: (UnturnedPlayer)caller;
target.Heal();
UnturnedChat.Say(caller, $"Healed {target.DisplayName}.", UnityEngine.Color.green);
}
catch (WrongUsageOfCommandException)
{
// Let RocketMod handle this -- it sends the usage message
throw;
}
catch (NoPermissionsForCommandException)
{
// Let RocketMod handle this -- it sends the permission denied message
throw;
}
catch (Exception ex)
{
Logger.LogException(ex);
UnturnedChat.Say(caller, "An error occurred. The server operator has been notified.", UnityEngine.Color.red);
}
}This pattern re-throws RocketMod's built-in exceptions so the framework handles the user-facing message, while catching unexpected exceptions and logging them.
Event handler error handling
Event handlers that throw unhandled exceptions can break the event chain. Other subscribers to the same event may not fire. Always wrap event handler bodies:
csharp
private void OnPlayerConnected(UnturnedPlayer player)
{
try
{
// Event logic
LoadPlayerData(player);
}
catch (Exception ex)
{
Logger.LogException(ex);
}
}Scheduled task error handling
Scheduled tasks are the most common source of silent failures. If a TaskDispatcher callback throws, the error is logged but the task stops executing -- it does not reschedule. Wrap the callback body:
csharp
TaskDispatcher.QueueOnMainThread(() =>
{
try
{
PerformCleanup();
}
catch (Exception ex)
{
Logger.LogException(ex);
}
}, 60000);Async method error handling
Async methods that throw unhandled exceptions terminate the task. If the method is async void (used for event handlers), the exception crashes the process. Always wrap async methods:
csharp
private async Task PerformDatabaseOperationAsync()
{
try
{
await _database.QueryAsync("...");
}
catch (Exception ex)
{
Logger.LogException(ex);
}
}Structured error context
A raw exception log entry tells you what went wrong but not which player was affected or what state the plugin was in. Attach context to every log entry:
csharp
public void LogErrorWithContext(Exception ex, UnturnedPlayer player, string operation)
{
string context = $"Operation: {operation} | " +
$"Player: {player?.DisplayName ?? "N/A"} | " +
$"SteamID: {player?.CSteamID ?? 0}";
Logger.Log("=== ERROR CONTEXT ===");
Logger.Log(context);
Logger.LogException(ex);
Logger.Log("=== END ERROR CONTEXT ===");
}Error context helper
csharp
public class ErrorContext
{
public string Operation { get; set; }
public string PlayerName { get; set; }
public ulong SteamId { get; set; }
public string CommandText { get; set; }
public string PluginState { get; set; }
public DateTime Timestamp { get; set; }
public void Log(Exception ex)
{
Logger.Log("=== Error Context ===");
Logger.Log($"Timestamp: {Timestamp:yyyy-MM-dd HH:mm:ss}");
Logger.Log($"Operation: {Operation}");
Logger.Log($"Player: {PlayerName} ({SteamId})");
Logger.Log($"Command: {CommandText}");
Logger.Log($"Plugin state: {PluginState}");
Logger.LogException(ex);
Logger.Log("=== End Error Context ===");
}
}Log file management
RocketMod writes log entries to Rocket.log in the server's root directory. The log file grows indefinitely and is not rotated by RocketMod itself. For long-running servers, the log file can reach hundreds of megabytes.
Log rotation in your plugin
Implement log rotation within your plugin to prevent unbounded log file growth:
csharp
public class LogManager
{
private readonly string _logDirectory;
private readonly int _maxLogSizeBytes;
private readonly int _maxArchiveFiles;
public LogManager(string logDirectory, int maxLogSizeMB = 50, int maxArchiveFiles = 5)
{
_logDirectory = logDirectory;
_maxLogSizeBytes = maxLogSizeMB * 1024 * 1024;
_maxArchiveFiles = maxArchiveFiles;
}
public void CheckRotation()
{
string logPath = Path.Combine(_logDirectory, "Rocket.log");
if (!File.Exists(logPath))
return;
FileInfo info = new FileInfo(logPath);
if (info.Length < _maxLogSizeBytes)
return;
RotateLog(logPath);
}
private void RotateLog(string logPath)
{
string timestamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss");
string archivePath = Path.Combine(_logDirectory, $"Rocket-{timestamp}.log");
File.Move(logPath, archivePath);
// Clean old archives
var archives = Directory.GetFiles(_logDirectory, "Rocket-*.log")
.OrderByDescending(f => f)
.Skip(_maxArchiveFiles);
foreach (string oldArchive in archives)
{
File.Delete(oldArchive);
}
Logger.Log($"Log rotated. Archived to {archivePath}");
}
}Logging to a separate file
If your plugin produces high-volume log output, write to a separate file instead of the shared Rocket.log:
csharp
public class PluginLogger
{
private readonly string _filePath;
private readonly object _lock = new object();
public PluginLogger(string pluginName)
{
string logDir = Path.Combine(Environment.CurrentDirectory, "Rocket", "Logs", pluginName);
Directory.CreateDirectory(logDir);
_filePath = Path.Combine(logDir, $"{DateTime.UtcNow:yyyy-MM-dd}.log");
}
public void Log(string message)
{
string line = $"[{DateTime.UtcNow:HH:mm:ss}] {message}";
lock (_lock)
{
File.AppendAllText(_filePath, line + Environment.NewLine);
}
}
public void LogException(Exception ex)
{
Log($"EXCEPTION: {ex.GetType().Name}: {ex.Message}");
Log($"STACK: {ex.StackTrace}");
if (ex.InnerException != null)
{
Log($"INNER: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}");
}
}
}Defensive programming patterns
Input validation at every public method
Every public method should validate its inputs before operating:
csharp
public void HealPlayer(UnturnedPlayer player)
{
if (player == null)
throw new ArgumentNullException(nameof(player));
if (player.Dead)
{
Logger.LogWarning($"Cannot heal {player.DisplayName} -- player is dead.");
return;
}
player.Heal();
}Null checks on external dependencies
When your plugin depends on another plugin or external service, check availability before calling:
csharp
public bool ProcessEconomyTransaction(ulong playerSteamId, decimal amount)
{
if (_economyPlugin == null)
{
Logger.LogWarning("Economy plugin is not available. Transaction skipped.");
return false;
}
try
{
return _economyPlugin.Withdraw(playerSteamId, amount);
}
catch (Exception ex)
{
Logger.LogException(ex);
return false;
}
}Player state validation
Before accessing player properties that may not be initialized, validate the player state:
csharp
public void ApplyBuff(UnturnedPlayer player)
{
if (player == null)
{
Logger.LogWarning("Cannot apply buff -- player is null.");
return;
}
if (player.Player == null)
{
Logger.LogWarning("Cannot apply buff -- player character not fully initialized.");
return;
}
// Safe to proceed
}Common error patterns and their solutions
NullReferenceException: player.Player is null
Cause: The player has connected but their character has not spawned yet. Accessing player.Player during the join sequence or very early in OnPlayerConnected returns null.
Solution: Delay player object access until the character is confirmed spawned. Use a short TaskDispatcher delay or check player.Player != null before access.
IndexOutOfRangeException: command array access
Cause: The command handler accesses command[1] or command[2] without checking command.Length first.
Solution: Validate the array length before accessing elements by index.
FileNotFoundException: missing config or translation file
Cause: The plugin expects a configuration or translation file that was not created. RocketMod creates the file on first run using LoadDefaults(), but if the file is deleted manually, the plugin must handle the missing file gracefully.
Solution: Use File.Exists() checks before reading configuration files, and recreate the file if it is missing.
TypeLoadException: version mismatch in shared library
Cause: A shared library referenced by the plugin has changed between builds. The plugin was compiled against one version but the server has a different version of the DLL.
Solution: Use assembly binding redirects in the server's configuration, or ensure all plugins are rebuilt against the same version of shared libraries.
Frequently asked questions
Should I log every command execution?
Log commands that modify server state (kick, ban, teleport, give item). Do not log read-only commands like /help or /list unless you are debugging an issue. High-volume logging degrades server performance.
Does Logger.LogException() include the inner exception?
Yes. The Logger.LogException() method recursively logs the inner exception chain. Each inner exception is logged with its type, message, and stack trace.
How do I prevent log spam from a misbehaving plugin?
If a plugin logs excessively, implement an in-memory rate limiter that suppresses duplicate log entries:
csharp
private Dictionary<string, DateTime> _lastLogEntries = new Dictionary<string, DateTime>();
public void RateLimitedLog(string key, string message, int cooldownMs)
{
if (_lastLogEntries.TryGetValue(key, out DateTime lastLog))
{
if ((DateTime.UtcNow - lastLog).TotalMilliseconds < cooldownMs)
return;
}
_lastLogEntries[key] = DateTime.UtcNow;
Logger.Log(message);
}Cross-references
- Inter-Plugin Communication -- the previous article; error propagation across plugin boundaries.
- Player Inventory Direct Access -- the next article; error handling for inventory operations.
- Scheduling Tasks and Background Work -- error handling in scheduled callbacks.
- Commands and IRocketCommand -- command-level exception handling patterns.
- Debugging Server Exceptions -- general server-side exception diagnostics.
What changed in this revision
- Removed
CommandNotFoundException-- not found in real RocketMod API surface. - Removed
NoPermissionException(invented name) -- real type isNoPermissionsForCommandException; article now uses the real name. - Removed
PluginCallException-- does not exist in the real RocketMod API surface. - Removed
TranslationNotFoundException-- does not exist in the real RocketMod API surface. - Removed
Logger.Log(LogType, string)overload section -- the real API surface does not show this overload; replaced withLogger.LogError()which exists as a separate method. - Removed
LogTypeenum values table -- the real enum isELogType, and its member values are not verifiable from the API surface. - Removed
Logger.OnLogevent --Loggerhas no events in the real API surface. - Removed
R.Permissions.HasPermission()code example --Rclass has noPermissionsproperty in the real API surface. - Removed
player.IsConnectednull check -- property does not exist onUnturnedPlayerin the real API surface. - Replaced
player.Player.life.serverModifyHealth()reference withplayer.Heal()-- SDG.Unturned-level call chain not in RocketMod API surface.
