Repository: kelektiv/node-cron
Pull request #940 (2025-01-09 05:54:49)

Feature/error handling
<!--- Provide a general summary of your changes in the Title above (following the Conventional Commits standard) -->
<!-- More infos: https://www.conventionalcommits.org -->
<!-- Commit types: https://github.com/insurgent-lab/conventional-changelog-preset#commit-types-->

## Description

<!--- Describe your changes in detail -->

## Related Issue

<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->

## How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

## Screenshots (if appropriate):

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] If my change introduces a breaking change, I have added a `!` after the type/scope in the title (see the Conventional Commits standard).


github-actions[bot]: Hey there and thank you for opening this pull request! 👋🏼

We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.

Details:

```
No release type found in pull request title "Feature/error handling". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/

Available types:
 - feat: A new feature
 - fix: A bug fix
 - docs: Documentation only changes
 - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
 - refactor: A code change that neither fixes a bug nor adds a feature
 - perf: A code change that improves performance
 - test: Adding missing tests or correcting existing tests
 - build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
 - ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
 - chore: Other changes that don't modify src or test files
 - revert: Reverts a previous commit
```
<!-- Sticky Pull Request Commentpr-title-lint-error -->

Patch:
diff --git a/README.md b/README.md
index 21b395a..30a0b3d 100644
--- a/README.md
+++ b/README.md
@@ -196,6 +196,8 @@ day of week    0-7 (0 or 7 is Sunday, or use names)
 
 - `waitForCompletion`: [OPTIONAL] - If `true`, no additional instances of the `onTick` callback function will run until the current onTick callback has completed. Any new scheduled executions that occur while the current callback is running will be skipped entirely. Default is `false`.
 
+- `errorHandler`: [OPTIONAL] - Function to handle any exceptions that occur in the `onTick` method.
+
 #### Methods
 
 - `from` (static): Create a new CronJob object providing arguments as an object. See argument names and descriptions above.
diff --git a/src/job.ts b/src/job.ts
index f051d1a..730d926 100644
--- a/src/job.ts
+++ b/src/job.ts
@@ -22,6 +22,7 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 		? CronOnCompleteCallback
 		: undefined;
 	waitForCompletion = false;
+	errorHandler?: CronJobParams<OC, C>['errorHandler'];
 
 	private _isCallbackRunning = false;
 	private _timeout?: NodeJS.Timeout;
@@ -41,7 +42,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 		runOnInit?: CronJobParams<OC, C>['runOnInit'],
 		utcOffset?: null,
 		unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
-		waitForCompletion?: CronJobParams<OC, C>['waitForCompletion']
+		waitForCompletion?: CronJobParams<OC, C>['waitForCompletion'],
+		errorHandler?: CronJobParams<OC, C>['errorHandler']
 	);
 	constructor(
 		cronTime: CronJobParams<OC, C>['cronTime'],
@@ -53,7 +55,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 		runOnInit?: CronJobParams<OC, C>['runOnInit'],
 		utcOffset?: CronJobParams<OC, C>['utcOffset'],
 		unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
-		waitForCompletion?: CronJobParams<OC, C>['waitForCompletion']
+		waitForCompletion?: CronJobParams<OC, C>['waitForCompletion'],
+		errorHandler?: CronJobParams<OC, C>['errorHandler']
 	);
 	constructor(
 		cronTime: CronJobParams<OC, C>['cronTime'],
@@ -65,11 +68,14 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 		runOnInit?: CronJobParams<OC, C>['runOnInit'],
 		utcOffset?: CronJobParams<OC, C>['utcOffset'],
 		unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
-		waitForCompletion?: CronJobParams<OC, C>['waitForCompletion']
+		waitForCompletion?: CronJobParams<OC, C>['waitForCompletion'],
+		errorHandler?: CronJobParams<OC, C>['errorHandler']
 	) {
 		this.context = (context ?? this) as CronContext<C>;
 		this.waitForCompletion = Boolean(waitForCompletion);
 
+		this.errorHandler = errorHandler;
+
 		// runtime check for JS users
 		if (timeZone != null && utcOffset != null) {
 			throw new ExclusiveParametersError('timeZone', 'utcOffset');
@@ -128,7 +134,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 				params.runOnInit,
 				params.utcOffset,
 				params.unrefTimeout,
-				params.waitForCompletion
+				params.waitForCompletion,
+				params.errorHandler
 			);
 		} else if (params.utcOffset != null) {
 			return new CronJob<OC, C>(
@@ -141,7 +148,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 				params.runOnInit,
 				params.utcOffset,
 				params.unrefTimeout,
-				params.waitForCompletion
+				params.waitForCompletion,
+				params.errorHandler
 			);
 		} else {
 			return new CronJob<OC, C>(
@@ -154,7 +162,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 				params.runOnInit,
 				params.utcOffset,
 				params.unrefTimeout,
-				params.waitForCompletion
+				params.waitForCompletion,
+				params.errorHandler
 			);
 		}
 	}
@@ -224,7 +233,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
 				if (this.waitForCompletion) await result;
 			}
 		} catch (error) {
-			console.error('[Cron] error in callback', error);
+			if (this.errorHandler != null) this.errorHandler(error);
+			else console.error('[Cron] error in callback', error);
 		} finally {
 			this._isCallbackRunning = false;
 		}
diff --git a/src/types/cron.types.ts b/src/types/cron.types.ts
index 4df8f09..dda5b34 100644
--- a/src/types/cron.types.ts
+++ b/src/types/cron.types.ts
@@ -16,6 +16,7 @@ interface BaseCronJobParams<
 	runOnInit?: boolean | null;
 	unrefTimeout?: boolean | null;
 	waitForCompletion?: boolean | null;
+	errorHandler?: ((error: unknown) => void) | null;
 }
 
 export type CronJobParams<


Test patch:
diff --git a/tests/cron.test.ts b/tests/cron.test.ts
index be72224..bd7a9e2 100644
--- a/tests/cron.test.ts
+++ b/tests/cron.test.ts
@@ -1151,6 +1151,44 @@ describe('cron', () => {
 		expect(callback).toHaveBeenCalledTimes(1);
 	});
 
+	it('should catch errors every time, if errorHandler is provided', () => {
+		const clock = sinon.useFakeTimers();
+		const errorFunc = jest.fn().mockImplementation(() => {
+			throw Error('Exception');
+		});
+		const handlerFunc = jest.fn();
+		const job = CronJob.from({
+			cronTime: '* * * * * *',
+			onTick: errorFunc,
+			errorHandler: handlerFunc,
+			start: true
+		});
+		clock.tick(1000);
+		expect(errorFunc).toHaveBeenCalledTimes(1);
+		expect(handlerFunc).toHaveBeenCalledTimes(1);
+		expect(handlerFunc).toHaveBeenLastCalledWith(new Error('Exception'));
+		clock.tick(1000);
+		expect(errorFunc).toHaveBeenCalledTimes(2);
+		expect(handlerFunc).toHaveBeenCalledTimes(2);
+		expect(handlerFunc).toHaveBeenLastCalledWith(new Error('Exception'));
+
+		job.stop();
+		clock.restore();
+	});
+
+	it('should throw errors if errorHandler is NOT provided', () => {
+		const errorFunc = jest.fn().mockImplementation(() => {
+			throw Error('Exception');
+		});
+		expect(() => {
+			CronJob.from({
+				cronTime: '* * * * * *',
+				onTick: errorFunc,
+				runOnInit: true
+			});
+		}).toThrow('Exception');
+	});
+
 	describe('waitForCompletion and job status tracking', () => {
 		it('should wait for async job completion when waitForCompletion is true', async () => {
 			const clock = sinon.useFakeTimers();