Skip to content

Commit b8d6166

Browse files
committed
Fix double callback invoke on unhandled exception
1 parent b673581 commit b8d6166

File tree

2 files changed

+53
-1
lines changed

2 files changed

+53
-1
lines changed

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ export function createOptionalCallbackFunction<T, A extends unknown[]>(
256256
if (isErrorFirstCallback(possibleCallback)) {
257257
try {
258258
const result = syncVersion(...(args.slice(0, -1) as A));
259-
possibleCallback(null, result);
259+
process.nextTick(() => possibleCallback(null, result));
260260
} catch (err) {
261261
possibleCallback(err instanceof Error ? err : new Error("Unknown error"));
262262
}

test/types-tests.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/// <reference lib="dom" />
2+
import * as types from "../src/types";
3+
import { expect } from "chai";
4+
5+
describe("createOptionalCallbackFunction", function () {
6+
it("should not execute callback twice when callback throws unhandled exception", function (done) {
7+
const syncFn = (a: number, b: number) => a + b;
8+
const flexibleFn = types.createOptionalCallbackFunction(syncFn);
9+
10+
let callbackExecutionCount = 0;
11+
12+
// Store and remove existing unhandled exception listeners
13+
const existingListeners = process.rawListeners("uncaughtException");
14+
process.removeAllListeners("uncaughtException");
15+
16+
process.once("uncaughtException", (err) => {
17+
// Restore unhandled exception listeners
18+
existingListeners.forEach((listener) => {
19+
process.on("uncaughtException", listener as NodeJS.UncaughtExceptionListener);
20+
});
21+
22+
expect(err.message).to.equal("Callback threw an error");
23+
expect(callbackExecutionCount).to.equal(1);
24+
done();
25+
});
26+
27+
flexibleFn(2, 3, (err, result) => {
28+
callbackExecutionCount++;
29+
expect(err).to.be.null;
30+
expect(result).to.equal(5);
31+
32+
throw new Error("Callback threw an error");
33+
});
34+
});
35+
36+
it("should defer callback execution in success case", function (done) {
37+
const syncFn = (a: number, b: number) => a + b;
38+
const flexibleFn = types.createOptionalCallbackFunction(syncFn);
39+
40+
let callbackExecuted = false;
41+
42+
flexibleFn(2, 3, (err, result) => {
43+
callbackExecuted = true;
44+
expect(err).to.be.null;
45+
expect(result).to.equal(5);
46+
done();
47+
});
48+
49+
// Callback should be asynchronously deferred
50+
expect(callbackExecuted).to.be.false;
51+
});
52+
});

0 commit comments

Comments
 (0)