A robust wrapper around node-cron with automatic retries, overlap prevention, execution timeout, history tracking, and structured error handling
npm install cron-safenode-cron jobs are vulnerable to:
onStart, onSuccess, onRetry, onError, onTimeout
bash
npm install cron-safe node-cron
`
> Note: node-cron is a peer dependency. You must install it separately.
Quick Start
`typescript
import { schedule } from 'cron-safe';
// Simple scheduled task
const task = schedule('/5 *', async () => {
const data = await fetchDataFromAPI();
await saveToDatabase(data);
return data; // Return value available via trigger()
});
// Stop when needed
task.stop();
`
Features
$3
`typescript
import { schedule } from 'cron-safe';
const task = schedule('0 ', async () => {
await unreliableApiCall();
}, {
retries: 3, // Retry up to 3 times
retryDelay: 5000, // Wait 5 seconds between retries
onRetry: (error, attempt) => {
console.log(Attempt ${attempt} failed:, error.message);
},
onError: (error) => {
// Called after all retries are exhausted
alertOpsTeam('Critical task failed!', error);
},
});
`
$3
Smart retry delays that grow over time, preventing thundering herd problems:
`typescript
import { schedule } from 'cron-safe';
const task = schedule('0 ', async () => {
await unreliableApiCall();
}, {
retries: 5,
retryDelay: 1000, // Base delay: 1 second
backoffStrategy: 'exponential', // 2s, 4s, 8s, 16s, 32s
maxRetryDelay: 30000, // Cap at 30 seconds
onRetry: (error, attempt) => {
console.log(Retry ${attempt}, next delay will be longer...);
},
});
// Available strategies:
// - 'fixed': Same delay every time (default)
// - 'linear': delay * attempt (1s, 2s, 3s, 4s, 5s)
// - 'exponential': delay * 2^attempt (2s, 4s, 8s, 16s, 32s)
`
$3
`typescript
import { schedule } from 'cron-safe';
// This task runs every minute but might take 90 seconds
const task = schedule(' *', async () => {
await longRunningDataSync(); // Takes ~90 seconds
}, {
preventOverlap: true, // Skip if previous run still executing
onOverlapSkip: () => {
console.log('Skipped: previous execution still running');
},
});
`
$3
Prevent zombie tasks from blocking future executions:
`typescript
import { schedule, TimeoutError } from 'cron-safe';
const task = schedule('/5 *', async () => {
await potentiallyHangingOperation();
}, {
executionTimeout: 30000, // 30 second timeout
onTimeout: (error) => {
console.error('Task timed out!', error.message);
// error instanceof TimeoutError === true
},
});
`
$3
Track past executions with status, duration, and errors:
`typescript
import { schedule } from 'cron-safe';
const task = schedule('0 ', async () => {
return await generateReport();
}, {
historyLimit: 20, // Keep last 20 executions (default: 10)
});
// Check execution history
const history = task.getHistory();
console.log(history);
// [
// {
// startedAt: Date,
// endedAt: Date,
// duration: 1234, // ms
// status: 'success' | 'failed' | 'timeout',
// error?: Error,
// triggeredBy: 'schedule' | 'manual'
// },
// ...
// ]
// Find failed executions
const failures = history.filter(h => h.status === 'failed');
`
$3
Know exactly when your job runs next:
`typescript
import { schedule } from 'cron-safe';
const task = schedule('0 9 *', async () => {
await sendDailyDigest();
});
const nextRun = task.nextRun();
console.log(Next run: ${nextRun}); // Date object or null if stopped
// Show in UI
const timeUntilNext = nextRun.getTime() - Date.now();
console.log(Next backup in ${Math.round(timeUntilNext / 60000)} minutes);
`
$3
Manually trigger tasks and get results (great for testing):
`typescript
import { schedule } from 'cron-safe';
const task = schedule('0 0 *', async () => {
const report = await generateDailyReport();
return report; // Return the result
});
// Manual trigger returns the result
const result = await task.trigger();
console.log('Report:', result);
// Respects overlap prevention
// If preventOverlap is true and task is running, returns undefined
`
$3
Integrate with Slack, email, or any notification system. The notifier callback receives structured payloads when events occur:
`typescript
import { schedule, NotificationPayload } from 'cron-safe';
const task = schedule('0 ', async () => {
await processData();
}, {
name: 'hourly-processor',
notifier: (payload: NotificationPayload) => {
console.log([${payload.taskName}] ${payload.event} at ${payload.timestamp});
// payload.result - for success events
// payload.error - for error/timeout events
// payload.duration - execution time in ms
// payload.attemptsMade - number of attempts
},
notifyOn: {
success: true, // default: true
error: true, // default: true
timeout: true, // default: true
overlapSkip: false, // default: false
},
});
`
#### Slack Webhook Adapter
`typescript
import { schedule, NotificationPayload } from 'cron-safe';
const slackNotifier = async (payload: NotificationPayload) => {
const color = payload.event === 'success' ? 'good' : 'danger';
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attachments: [{
color,
title: Cron Job: ${payload.taskName},
text: Event: ${payload.event.toUpperCase()},
fields: [
{ title: 'Duration', value: ${payload.duration}ms, short: true },
{ title: 'Attempts', value: payload.attemptsMade, short: true },
],
ts: Math.floor(payload.timestamp.getTime() / 1000),
}],
}),
});
};
const task = schedule('0 6 *', dailyBackup, {
name: 'daily-backup',
notifier: slackNotifier,
notifyOn: { success: false, error: true }, // Only notify on failures
});
`
#### Email Adapter (using nodemailer)
`typescript
import { schedule, NotificationPayload } from 'cron-safe';
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: 'smtp.example.com',
auth: { user: 'user', pass: 'pass' },
});
const emailNotifier = async (payload: NotificationPayload) => {
if (payload.event !== 'error' && payload.event !== 'timeout') return;
await transporter.sendMail({
from: 'cron@example.com',
to: 'ops-team@example.com',
subject: [ALERT] ${payload.taskName} ${payload.event},
text:
,
});
};
const task = schedule('0 0 *', nightlyCleanup, {
name: 'nightly-cleanup',
notifier: emailNotifier,
});
`
$3
`typescript
import { schedule } from 'cron-safe';
const task = schedule('0 9 *', async () => {
return await generateDailyReport();
}, {
name: 'daily-report',
retries: 2,
retryDelay: 10000,
preventOverlap: true,
executionTimeout: 60000,
historyLimit: 50,
onStart: () => {
console.log('[daily-report] Starting execution');
},
onSuccess: (result) => {
console.log('[daily-report] Completed:', result);
},
onRetry: (error, attempt) => {
console.warn([daily-report] Retry ${attempt}:, error.message);
},
onError: (error) => {
console.error('[daily-report] Failed permanently:', error);
sendSlackAlert('Daily report generation failed!');
},
onTimeout: (error) => {
console.error('[daily-report] Timed out:', error.message);
},
onOverlapSkip: () => {
console.warn('[daily-report] Skipped due to overlap');
},
});
`
API
$3
Schedules a task with automatic retries, timeout, and overlap prevention.
Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| cronExpression | string | A valid cron expression (e.g., ' *') |
| task | () => T \| Promise | The function to execute |
| options | CronSafeOptions | Configuration options (see below) |
Returns: CronSafeTask
$3
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| name | string | undefined | Identifier for logging/debugging |
| retries | number | 0 | Number of retry attempts after failure |
| retryDelay | number | 0 | Base delay in ms between retries |
| backoffStrategy | 'fixed' \| 'linear' \| 'exponential' | 'fixed' | How delay grows between retries |
| maxRetryDelay | number | undefined | Maximum delay cap for backoff |
| preventOverlap | boolean | false | Skip execution if previous run is active |
| executionTimeout | number | undefined | Max execution time in ms before timeout |
| historyLimit | number | 10 | Max number of history entries to keep |
| onStart | () => void | — | Called when task starts |
| onSuccess | (result: T) => void | — | Called with result on success |
| onRetry | (error, attempt) => void | — | Called before each retry |
| onError | (error) => void | — | Called when all retries exhausted |
| onTimeout | (error: Error) => void | — | Called when task times out |
| onOverlapSkip | () => void | — | Called when execution is skipped |
| notifier | Notifier | — | Callback for Slack/email/custom notifications |
| notifyOn | NotifyOn | { success: true, error: true, timeout: true, overlapSkip: false } | Which events trigger notifications |
| timezone | string | — | Timezone for cron schedule |
| scheduled | boolean | true | Start immediately or wait for .start() |
| runOnInit | boolean | false | Run task immediately on creation |
$3
The object returned by schedule():
| Method | Returns | Description |
|--------|---------|-------------|
| start() | void | Start the scheduled task |
| stop() | void | Stop the scheduled task |
| getStatus() | 'scheduled' \| 'running' \| 'stopped' | Current status |
| trigger() | Promise | Execute immediately, returns result |
| getHistory() | RunHistory[] | Get execution history (newest first) |
| nextRun() | Date \| null | Next scheduled run time |
$3
| Property | Type | Description |
|----------|------|-------------|
| startedAt | Date | When execution started |
| endedAt | Date \| undefined | When execution ended |
| duration | number \| undefined | Duration in milliseconds |
| status | 'running' \| 'success' \| 'failed' \| 'timeout' | Execution status |
| error | Error \| undefined | Error if failed/timeout |
| triggeredBy | 'schedule' \| 'manual' | How the run was triggered |
$3
Validates a cron expression. Re-exported from node-cron.
`typescript
import { validate } from 'cron-safe';
console.log(validate(' *')); // true
console.log(validate('invalid')); // false
`
$3
Error class thrown when a task exceeds its execution timeout.
`typescript
import { TimeoutError } from 'cron-safe';
// In your onError handler
onError: (error) => {
if (error instanceof TimeoutError) {
console.log('Task timed out');
}
}
`
Migration from node-cron
Before:
`typescript
import cron from 'node-cron';
cron.schedule(' *', async () => {
await myTask(); // Errors go unhandled!
});
`
After:
`typescript
import { schedule } from 'cron-safe';
schedule(' *', async () => {
await myTask();
}, {
retries: 3,
executionTimeout: 30000,
onError: (err) => console.error('Task failed:', err),
});
`
TypeScript
Full TypeScript support with strict types:
`typescript
import { schedule, CronSafeOptions, CronSafeTask, RunHistory } from 'cron-safe';
interface ReportResult {
rowsProcessed: number;
duration: number;
}
const options: CronSafeOptions = {
retries: 2,
executionTimeout: 60000,
historyLimit: 100,
onSuccess: (result) => {
// result is typed as ReportResult
console.log( Processed ${result.rowsProcessed} rows);
},
};
const task: CronSafeTask = schedule('0 ', async (): Promise => {
return { rowsProcessed: 1000, duration: 5000 };
}, options);
// Trigger returns typed result
const result = await task.trigger();
if (result) {
console.log(result.rowsProcessed); // TypeScript knows this is a number
}
// History is also typed
const history: RunHistory[] = task.getHistory();
``