CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/golang-github-com-go-co-op-gocron-v2

A Golang job scheduling library that lets you run Go functions at pre-determined intervals using cron expressions, fixed durations, daily, weekly, monthly, or one-time schedules with support for distributed deployments.

Overview
Eval results
Files

error-handling.mddocs/examples/by-feature/

Error Handling Examples

Handle errors in gocron v2 jobs using error checking, event listeners, and retry patterns.

Basic Error Checking

Check Job Creation Errors

import "errors"

j, err := s.NewJob(
    gocron.CronJob("invalid cron", false),
    gocron.NewTask(myFunc),
)
if err != nil {
    if errors.Is(err, gocron.ErrCronJobParse) {
        fmt.Println("Invalid cron expression")
    } else {
        fmt.Println("Failed to create job:", err)
    }
}

Validate Configuration Before Job Creation

cronExpr := "0 9 * * *" // From config or user input

_, err := s.NewJob(
    gocron.CronJob(cronExpr, false),
    gocron.NewTask(myFunc),
)
if err != nil {
    log.Fatalf("Invalid cron expression: %v", err)
}

Handle Scheduler Creation Errors

loc, err := time.LoadLocation("Invalid/Timezone")
if err != nil {
    log.Fatalf("Invalid timezone: %v", err)
}

s, err := gocron.NewScheduler(gocron.WithLocation(loc))
if err != nil {
    log.Fatalf("Failed to create scheduler: %v", err)
}
defer s.Shutdown()

Task Error Handling

Return Errors from Tasks

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func() error {
        // Task can return error
        return doWorkThatMayFail()
    }),
    gocron.WithName("error-returning-task"),
)

Task with Multiple Return Values

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func() (string, error) {
        result, err := fetchData()
        if err != nil {
            return "", fmt.Errorf("fetch failed: %w", err)
        }
        return result, nil
    }),
    gocron.WithName("multi-return-task"),
)

Context-Aware Error Handling

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func(ctx context.Context) error {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(doWork()):
            return nil
        }
    }),
    gocron.WithName("context-aware-task"),
)

Event Listeners for Errors

Basic Error Listener

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func() error {
        return doWorkThatMayFail()
    }),
    gocron.WithEventListeners(
        gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
            fmt.Printf("[%s] Failed: %v\n", jobName, err)
        }),
    ),
)

Comprehensive Error Logging

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func() error {
        return performTask()
    }),
    gocron.WithName("comprehensive-logging"),
    gocron.WithEventListeners(
        gocron.BeforeJobRuns(func(jobID uuid.UUID, jobName string) {
            log.Printf("[%s] Starting at %v", jobName, time.Now())
        }),
        gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) {
            log.Printf("[%s] Completed successfully", jobName)
        }),
        gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
            log.Printf("[%s] Failed with error: %v", jobName, err)
            // Could also send to error tracking service
            sendToErrorTracking(jobName, err)
        }),
    ),
)

Error Categorization

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func() error {
        return performTask()
    }),
    gocron.WithName("categorized-errors"),
    gocron.WithEventListeners(
        gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
            // Categorize errors
            if errors.Is(err, context.Canceled) {
                log.Printf("[%s] Cancelled: %v", jobName, err)
            } else if errors.Is(err, context.DeadlineExceeded) {
                log.Printf("[%s] Timeout: %v", jobName, err)
                alertOnTimeout(jobName)
            } else if isNetworkError(err) {
                log.Printf("[%s] Network error: %v", jobName, err)
                retryLater(jobName)
            } else {
                log.Printf("[%s] Unknown error: %v", jobName, err)
                alertCritical(jobName, err)
            }
        }),
    ),
)

Panic Recovery

Automatic Panic Recovery

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func() {
        panic("something went wrong!")
    }),
    gocron.WithEventListeners(
        gocron.AfterJobRunsWithPanic(func(jobID uuid.UUID, jobName string, recoverData any) {
            fmt.Printf("[%s] Panicked: %v\n", jobName, recoverData)
        }),
    ),
)
// gocron automatically recovers panics

Panic with Stack Trace

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func() {
        // This will panic
        var x *int
        *x = 5
    }),
    gocron.WithName("panic-job"),
    gocron.WithEventListeners(
        gocron.AfterJobRunsWithPanic(func(jobID uuid.UUID, jobName string, recoverData any) {
            // Log panic with stack trace
            stack := debug.Stack()
            log.Printf("[%s] PANIC: %v\nStack:\n%s", jobName, recoverData, stack)

            // Send to error tracking
            sendPanicToTracking(jobName, recoverData, stack)
        }),
    ),
)

Conditional Execution

Skip Job on Pre-Condition Failure

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(doBackup),
    gocron.WithEventListeners(
        gocron.BeforeJobRunsSkipIfBeforeFuncErrors(func(jobID uuid.UUID, jobName string) error {
            if !isDatabaseReady() {
                return errors.New("database not ready")
            }
            return nil
        }),
    ),
)
// Job is skipped if before-func returns error

Pre-Flight Checks

j, _ := s.NewJob(
    gocron.DurationJob(5*time.Minute),
    gocron.NewTask(func() error {
        return syncData()
    }),
    gocron.WithName("data-sync"),
    gocron.WithEventListeners(
        gocron.BeforeJobRunsSkipIfBeforeFuncErrors(func(jobID uuid.UUID, jobName string) error {
            // Check prerequisites
            if !isNetworkAvailable() {
                return errors.New("network unavailable")
            }
            if !hasValidCredentials() {
                return errors.New("credentials invalid")
            }
            if getCurrentLoad() > 0.9 {
                return errors.New("system load too high")
            }
            return nil
        }),
        gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
            log.Printf("[%s] Sync failed: %v", jobName, err)
        }),
    ),
)

Retry Patterns

Manual Retry with Limited Attempts

j, _ := s.NewJob(
    gocron.DurationJob(5*time.Minute),
    gocron.NewTask(func() error {
        maxRetries := 3
        var lastErr error

        for attempt := 1; attempt <= maxRetries; attempt++ {
            err := attemptTask()
            if err == nil {
                return nil // Success
            }

            lastErr = err
            log.Printf("Attempt %d/%d failed: %v", attempt, maxRetries, err)

            if attempt < maxRetries {
                backoff := time.Duration(attempt) * time.Second
                time.Sleep(backoff)
            }
        }

        return fmt.Errorf("all %d attempts failed: %w", maxRetries, lastErr)
    }),
    gocron.WithName("retry-task"),
)

Exponential Backoff Retry

j, _ := s.NewJob(
    gocron.DurationJob(10*time.Minute),
    gocron.NewTask(func() error {
        maxRetries := 5
        baseDelay := 1 * time.Second

        for attempt := 1; attempt <= maxRetries; attempt++ {
            err := performNetworkOperation()
            if err == nil {
                return nil
            }

            log.Printf("Attempt %d failed: %v", attempt, err)

            if attempt < maxRetries {
                // Exponential backoff: 1s, 2s, 4s, 8s, 16s
                delay := baseDelay * (1 << (attempt - 1))
                time.Sleep(delay)
            }
        }

        return errors.New("max retries exceeded")
    }),
    gocron.WithName("exponential-backoff"),
)

Retry Job with LimitedRuns

// Try immediately, then at intervals, max 3 times
j, _ := s.NewJob(
    gocron.DurationJob(5*time.Minute),
    gocron.NewTask(func() error {
        return attemptConnection()
    }),
    gocron.WithStartAt(gocron.WithStartImmediately()),
    gocron.WithLimitedRuns(3),
    gocron.WithName("connection-retry"),
    gocron.WithEventListeners(
        gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
            log.Printf("[%s] Connection attempt failed: %v", jobName, err)
        }),
    ),
)
// Runs at most 3 times, then removed

Retry with Jitter

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func() error {
        maxRetries := 3
        baseDelay := 1 * time.Second
        maxJitter := 500 * time.Millisecond

        for attempt := 1; attempt <= maxRetries; attempt++ {
            err := callAPI()
            if err == nil {
                return nil
            }

            if attempt < maxRetries {
                // Add jitter to prevent thundering herd
                jitter := time.Duration(rand.Int63n(int64(maxJitter)))
                delay := baseDelay * time.Duration(attempt) + jitter
                time.Sleep(delay)
            }
        }

        return errors.New("max retries exceeded")
    }),
    gocron.WithName("retry-with-jitter"),
)

Shutdown Timeout Handling

Handle Shutdown Timeout

s, _ := gocron.NewScheduler(
    gocron.WithStopTimeout(5*time.Second),
)

// Add long-running job
s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func() {
        time.Sleep(10 * time.Second)
    }),
)

s.Start()

// Shutdown
err := s.Shutdown()
if errors.Is(err, gocron.ErrStopSchedulerTimedOut) {
    fmt.Println("Some jobs did not finish in time")
}

Graceful Shutdown with Context

s, _ := gocron.NewScheduler(
    gocron.WithStopTimeout(30*time.Second),
)

ctx, cancel := context.WithCancel(context.Background())

s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(func(ctx context.Context) error {
        for {
            select {
            case <-ctx.Done():
                log.Println("Job cancelled, cleaning up...")
                cleanup()
                return ctx.Err()
            default:
                if !processNextItem() {
                    return nil
                }
            }
        }
    }),
    gocron.WithContext(ctx),
)

s.Start()

// Later: graceful shutdown
cancel() // Cancel contexts first
err := s.Shutdown()
if err != nil {
    log.Printf("Shutdown error: %v", err)
}

Lock Errors

Handle Distributed Lock Failures

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(doWork),
    gocron.WithName("distributed-job"),
    gocron.WithEventListeners(
        gocron.AfterLockError(func(jobID uuid.UUID, jobName string, err error) {
            log.Printf("[%s] Failed to acquire lock: %v", jobName, err)

            // Track lock contention
            metrics.IncrementLockFailure(jobName)

            // Alert if lock failures are too frequent
            if metrics.GetLockFailureRate(jobName) > 0.5 {
                alertHighLockContention(jobName)
            }
        }),
    ),
)

Complete Error Handling Example

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/go-co-op/gocron/v2"
)

func main() {
    // Create scheduler with timeout
    s, err := gocron.NewScheduler(
        gocron.WithLocation(time.UTC),
        gocron.WithLogger(gocron.NewLogger(gocron.LogLevelInfo)),
        gocron.WithStopTimeout(30*time.Second),
    )
    if err != nil {
        log.Fatalf("Failed to create scheduler: %v", err)
    }
    defer func() {
        if err := s.Shutdown(); err != nil {
            log.Printf("Shutdown error: %v", err)
        }
    }()

    // Job with comprehensive error handling
    _, err = s.NewJob(
        gocron.DurationJob(30*time.Second),
        gocron.NewTask(func(ctx context.Context) error {
            // Simulate work that might fail
            select {
            case <-ctx.Done():
                return ctx.Err()
            case <-time.After(5 * time.Second):
                if time.Now().Unix()%3 == 0 {
                    return errors.New("simulated error")
                }
                return nil
            }
        }),
        gocron.WithName("error-prone-task"),
        gocron.WithSingletonMode(gocron.LimitModeReschedule),
        gocron.WithEventListeners(
            gocron.BeforeJobRuns(func(jobID uuid.UUID, jobName string) {
                log.Printf("[%s] Starting at %v", jobName, time.Now())
            }),
            gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) {
                log.Printf("[%s] Completed successfully", jobName)
            }),
            gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
                // Categorize and handle errors
                if errors.Is(err, context.Canceled) {
                    log.Printf("[%s] Cancelled", jobName)
                } else if errors.Is(err, context.DeadlineExceeded) {
                    log.Printf("[%s] Timeout", jobName)
                } else {
                    log.Printf("[%s] Error: %v", jobName, err)
                }
            }),
            gocron.AfterJobRunsWithPanic(func(jobID uuid.UUID, jobName string, recoverData any) {
                log.Printf("[%s] PANIC: %v", jobName, recoverData)
            }),
        ),
    )
    if err != nil {
        log.Fatalf("Failed to create job: %v", err)
    }

    // Job with pre-condition checks
    _, err = s.NewJob(
        gocron.DurationJob(time.Minute),
        gocron.NewTask(func() error {
            fmt.Println("Conditional job running")
            return nil
        }),
        gocron.WithName("conditional-job"),
        gocron.WithEventListeners(
            gocron.BeforeJobRunsSkipIfBeforeFuncErrors(func(jobID uuid.UUID, jobName string) error {
                // Check prerequisites
                if !checkSystemReady() {
                    return errors.New("system not ready")
                }
                return nil
            }),
        ),
    )
    if err != nil {
        log.Fatalf("Failed to create conditional job: %v", err)
    }

    // Job with retry logic
    _, err = s.NewJob(
        gocron.DurationJob(2*time.Minute),
        gocron.NewTask(func() error {
            maxRetries := 3
            for attempt := 1; attempt <= maxRetries; attempt++ {
                err := performOperation()
                if err == nil {
                    return nil
                }

                log.Printf("Attempt %d/%d failed: %v", attempt, maxRetries, err)

                if attempt < maxRetries {
                    time.Sleep(time.Second * time.Duration(attempt))
                }
            }
            return errors.New("max retries exceeded")
        }),
        gocron.WithName("retry-job"),
    )
    if err != nil {
        log.Fatalf("Failed to create retry job: %v", err)
    }

    // Start scheduler
    s.Start()
    log.Println("Scheduler started")

    // Wait for interrupt
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
    <-sigCh

    log.Println("Shutting down gracefully...")
}

func checkSystemReady() bool {
    return true // Placeholder
}

func performOperation() error {
    // Simulated operation
    if rand.Float32() < 0.3 {
        return errors.New("operation failed")
    }
    return nil
}

Best Practices

  1. Always check errors from NewJob and NewScheduler
  2. Use event listeners for error logging and monitoring
  3. Categorize errors for appropriate handling
  4. Implement retry logic for transient failures
  5. Use exponential backoff to avoid overwhelming services
  6. Add jitter to prevent thundering herd
  7. Set appropriate timeouts for graceful shutdown
  8. Monitor error rates to detect issues early
  9. Use context for cancellation support
  10. Log panics with stack traces for debugging

Related Documentation

  • API: Job Options — Event listeners
  • API: Types — Error types
  • Guide: Observability
  • Examples: Monitoring

Install with Tessl CLI

npx tessl i tessl/golang-github-com-go-co-op-gocron-v2

docs

index.md

tile.json