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.
Handle errors in gocron v2 jobs using error checking, event listeners, and retry patterns.
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)
}
}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)
}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()j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() error {
// Task can return error
return doWorkThatMayFail()
}),
gocron.WithName("error-returning-task"),
)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"),
)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"),
)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)
}),
),
)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)
}),
),
)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)
}
}),
),
)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 panicsj, _ := 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)
}),
),
)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 errorj, _ := 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)
}),
),
)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"),
)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"),
)// 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 removedj, _ := 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"),
)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")
}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)
}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)
}
}),
),
)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
}NewJob and NewSchedulerInstall with Tessl CLI
npx tessl i tessl/golang-github-com-go-co-op-gocron-v2docs
api
examples
guides