Skip to main content

Overview

gopsutil returns standard Go errors that provide context about failures. Understanding these errors and how to handle them properly is crucial for building robust applications.

Common Error Types

Not Implemented Error

Returned when a feature is not available on the current platform:
import (
    "errors"
    "github.com/shirou/gopsutil/v4/cpu"
    "github.com/shirou/gopsutil/v4/internal/common"
)

func getCPUInfo(ctx context.Context) error {
    info, err := cpu.InfoWithContext(ctx)
    if err != nil {
        if errors.Is(err, common.ErrNotImplementedError) {
            log.Println("CPU info not available on this platform")
            return nil // Handle gracefully
        }
        return fmt.Errorf("failed to get CPU info: %w", err)
    }
    
    // Process info...
    return nil
}

Context Errors

When using context-aware functions, handle context cancellation and timeouts:
import (
    "context"
    "errors"
    "time"
    "github.com/shirou/gopsutil/v4/process"
)

func getProcessInfo(pid int32) error {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    p, err := process.NewProcessWithContext(ctx, pid)
    if err != nil {
        return err
    }
    
    name, err := p.NameWithContext(ctx)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return fmt.Errorf("timeout getting process name: %w", err)
        }
        if errors.Is(err, context.Canceled) {
            return fmt.Errorf("operation canceled: %w", err)
        }
        return err
    }
    
    fmt.Println("Process name:", name)
    return nil
}

Process Errors

Specific errors for process operations:
import (
    "errors"
    "github.com/shirou/gopsutil/v4/process"
)

func checkProcess(pid int32) error {
    p, err := process.NewProcess(pid)
    if err != nil {
        return fmt.Errorf("failed to create process: %w", err)
    }
    
    running, err := p.IsRunning()
    if err != nil {
        if errors.Is(err, process.ErrorProcessNotRunning) {
            log.Printf("Process %d is not running", pid)
            return nil
        }
        return err
    }
    
    if !running {
        return process.ErrorProcessNotRunning
    }
    
    return nil
}

Permission Errors

Handle permission-related failures:
import (
    "errors"
    "os"
    "github.com/shirou/gopsutil/v4/process"
)

func getProcessDetails(pid int32) error {
    p, err := process.NewProcess(pid)
    if err != nil {
        return err
    }
    
    cmdline, err := p.Cmdline()
    if err != nil {
        // Check for permission errors
        if errors.Is(err, os.ErrPermission) || 
           errors.Is(err, process.ErrorNotPermitted) {
            log.Printf("Permission denied for process %d", pid)
            return nil // Handle gracefully
        }
        return fmt.Errorf("failed to get cmdline: %w", err)
    }
    
    fmt.Println("Command:", cmdline)
    return nil
}

Error Wrapping

Always wrap errors with context using fmt.Errorf and %w:
import (
    "fmt"
    "github.com/shirou/gopsutil/v4/cpu"
    "github.com/shirou/gopsutil/v4/mem"
)

func getSystemMetrics(ctx context.Context) error {
    // Wrap errors with context
    cpuPercent, err := cpu.PercentWithContext(ctx, 0, false)
    if err != nil {
        return fmt.Errorf("failed to get CPU percent: %w", err)
    }
    
    vmem, err := mem.VirtualMemoryWithContext(ctx)
    if err != nil {
        return fmt.Errorf("failed to get virtual memory: %w", err)
    }
    
    // Use metrics...
    _ = cpuPercent
    _ = vmem
    
    return nil
}

// Higher-level function can unwrap and inspect
func collectMetrics() {
    if err := getSystemMetrics(context.Background()); err != nil {
        // Can use errors.Is() or errors.As() to check wrapped errors
        log.Printf("Error collecting metrics: %v", err)
    }
}

Handling Multiple Errors

When collecting data from multiple sources, handle partial failures:
import (
    "sync"
    "github.com/shirou/gopsutil/v4/cpu"
    "github.com/shirou/gopsutil/v4/mem"
    "github.com/shirou/gopsutil/v4/disk"
)

type SystemMetrics struct {
    CPUPercent  []float64
    Memory      *mem.VirtualMemoryStat
    DiskUsage   *disk.UsageStat
    Errors      []error
}

func collectAllMetrics(ctx context.Context) *SystemMetrics {
    metrics := &SystemMetrics{
        Errors: make([]error, 0),
    }
    
    var wg sync.WaitGroup
    var mu sync.Mutex
    
    // Collect CPU metrics
    wg.Add(1)
    go func() {
        defer wg.Done()
        cpu, err := cpu.PercentWithContext(ctx, 0, false)
        mu.Lock()
        defer mu.Unlock()
        if err != nil {
            metrics.Errors = append(metrics.Errors, 
                fmt.Errorf("cpu: %w", err))
        } else {
            metrics.CPUPercent = cpu
        }
    }()
    
    // Collect memory metrics
    wg.Add(1)
    go func() {
        defer wg.Done()
        mem, err := mem.VirtualMemoryWithContext(ctx)
        mu.Lock()
        defer mu.Unlock()
        if err != nil {
            metrics.Errors = append(metrics.Errors, 
                fmt.Errorf("memory: %w", err))
        } else {
            metrics.Memory = mem
        }
    }()
    
    // Collect disk metrics
    wg.Add(1)
    go func() {
        defer wg.Done()
        disk, err := disk.UsageWithContext(ctx, "/")
        mu.Lock()
        defer mu.Unlock()
        if err != nil {
            metrics.Errors = append(metrics.Errors, 
                fmt.Errorf("disk: %w", err))
        } else {
            metrics.DiskUsage = disk
        }
    }()
    
    wg.Wait()
    return metrics
}

func main() {
    metrics := collectAllMetrics(context.Background())
    
    if len(metrics.Errors) > 0 {
        log.Println("Encountered errors:")
        for _, err := range metrics.Errors {
            log.Printf("  - %v", err)
        }
    }
    
    // Use whatever metrics we successfully collected
    if metrics.CPUPercent != nil {
        fmt.Printf("CPU: %.2f%%\n", metrics.CPUPercent[0])
    }
    if metrics.Memory != nil {
        fmt.Printf("Memory: %.2f%%\n", metrics.Memory.UsedPercent)
    }
}

Retry Logic

Implement retry logic for transient failures:
import (
    "context"
    "time"
    "github.com/shirou/gopsutil/v4/cpu"
)

func getCPUPercentWithRetry(ctx context.Context, maxRetries int) ([]float64, error) {
    var lastErr error
    
    for i := 0; i < maxRetries; i++ {
        percent, err := cpu.PercentWithContext(ctx, 100*time.Millisecond, false)
        if err == nil {
            return percent, nil
        }
        
        lastErr = err
        
        // Don't retry on context errors or not implemented
        if errors.Is(err, context.Canceled) ||
           errors.Is(err, context.DeadlineExceeded) ||
           errors.Is(err, common.ErrNotImplementedError) {
            return nil, err
        }
        
        // Exponential backoff
        if i < maxRetries-1 {
            backoff := time.Duration(i+1) * 100 * time.Millisecond
            select {
            case <-time.After(backoff):
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }
    }
    
    return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}

Logging Best Practices

Provide useful context in error messages:
import (
    "log/slog"
    "github.com/shirou/gopsutil/v4/process"
)

func monitorProcess(pid int32, logger *slog.Logger) error {
    p, err := process.NewProcess(pid)
    if err != nil {
        logger.Error("failed to create process",
            "pid", pid,
            "error", err)
        return err
    }
    
    cpuPercent, err := p.CPUPercent()
    if err != nil {
        logger.Warn("failed to get CPU percent",
            "pid", pid,
            "error", err)
        // Continue with other metrics
    } else {
        logger.Info("process CPU usage",
            "pid", pid,
            "cpu_percent", cpuPercent)
    }
    
    return nil
}

Custom Error Types

Create domain-specific errors for your application:
import (
    "errors"
    "fmt"
)

var (
    ErrSystemOverloaded = errors.New("system resources overloaded")
    ErrThresholdExceeded = errors.New("threshold exceeded")
)

type MetricError struct {
    Metric string
    Value  float64
    Err    error
}

func (e *MetricError) Error() string {
    return fmt.Sprintf("%s metric error (value: %.2f): %v", 
        e.Metric, e.Value, e.Err)
}

func (e *MetricError) Unwrap() error {
    return e.Err
}

func checkCPUUsage(ctx context.Context, threshold float64) error {
    percent, err := cpu.PercentWithContext(ctx, 0, false)
    if err != nil {
        return &MetricError{
            Metric: "cpu",
            Value:  0,
            Err:    err,
        }
    }
    
    if len(percent) > 0 && percent[0] > threshold {
        return &MetricError{
            Metric: "cpu",
            Value:  percent[0],
            Err:    ErrThresholdExceeded,
        }
    }
    
    return nil
}

Error Recovery

Implement graceful degradation when errors occur:
type MetricsCollector struct {
    lastValidCPU    []float64
    lastValidMemory *mem.VirtualMemoryStat
    logger          *slog.Logger
}

func (m *MetricsCollector) GetCPU(ctx context.Context) []float64 {
    cpu, err := cpu.PercentWithContext(ctx, 0, false)
    if err != nil {
        m.logger.Warn("failed to get current CPU, using cached value",
            "error", err)
        return m.lastValidCPU
    }
    
    m.lastValidCPU = cpu
    return cpu
}

func (m *MetricsCollector) GetMemory(ctx context.Context) *mem.VirtualMemoryStat {
    mem, err := mem.VirtualMemoryWithContext(ctx)
    if err != nil {
        m.logger.Warn("failed to get current memory, using cached value",
            "error", err)
        return m.lastValidMemory
    }
    
    m.lastValidMemory = mem
    return mem
}

Testing Error Handling

func TestErrorHandling(t *testing.T) {
    tests := []struct {
        name    string
        ctx     context.Context
        wantErr error
    }{
        {
            name:    "timeout",
            ctx:     func() context.Context {
                ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
                defer cancel()
                time.Sleep(10 * time.Millisecond)
                return ctx
            }(),
            wantErr: context.DeadlineExceeded,
        },
        {
            name:    "canceled",
            ctx:     func() context.Context {
                ctx, cancel := context.WithCancel(context.Background())
                cancel()
                return ctx
            }(),
            wantErr: context.Canceled,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := cpu.PercentWithContext(tt.ctx, 100*time.Millisecond, false)
            if !errors.Is(err, tt.wantErr) {
                t.Errorf("expected error %v, got %v", tt.wantErr, err)
            }
        })
    }
}

Best Practices Summary

1

Always check errors

Never ignore errors from gopsutil functions.
2

Use errors.Is() for comparison

Use errors.Is() to check for specific error types.
3

Wrap errors with context

Use fmt.Errorf with %w to preserve error chains.
4

Handle context cancellation

Always check for context.Canceled and context.DeadlineExceeded.
5

Implement graceful degradation

Continue operating with reduced functionality when possible.
6

Log errors with context

Include relevant details (PID, metric name, etc.) in error logs.

Cross-Platform

Handle platform-specific errors

Performance

Error handling impact on performance

Build docs developers (and LLMs) love