Skip to content

Commit bb645c2

Browse files
author
Julien Gilli
committed
domains: emit uncaughtException when appropriate
Fix node exiting due to an exception being thrown rather than emitting an `'uncaughtException'` event on the process object when no error handler is set on the domain within which an error is thrown and an `'uncaughtException'` event listener is set on the process. Fixes #3607.
1 parent 6d6bc5d commit bb645c2

File tree

2 files changed

+217
-8
lines changed

2 files changed

+217
-8
lines changed

lib/domain.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,20 @@ Domain.prototype._errorHandler = function errorHandler(er) {
9696
// that these exceptions are caught, and thus would prevent it from
9797
// aborting in these cases.
9898
if (stack.length === 1) {
99-
try {
100-
// Set the _emittingTopLevelDomainError so that we know that, even
101-
// if technically the top-level domain is still active, it would
102-
// be ok to abort on an uncaught exception at this point
103-
process._emittingTopLevelDomainError = true;
104-
caught = emitError();
105-
} finally {
106-
process._emittingTopLevelDomainError = false;
99+
// If there's no error handler, do not emit an 'error' event
100+
// as this would throw an error, make the process exit, and thus
101+
// prevent the process 'uncaughtException' event from being emitted
102+
// if a listener is set.
103+
if (self.listeners('error').length > 0) {
104+
try {
105+
// Set the _emittingTopLevelDomainError so that we know that, even
106+
// if technically the top-level domain is still active, it would
107+
// be ok to abort on an uncaught exception at this point
108+
process._emittingTopLevelDomainError = true;
109+
caught = emitError();
110+
} finally {
111+
process._emittingTopLevelDomainError = false;
112+
}
107113
}
108114
} else {
109115
// wrap this in a try/catch so we don't get infinite throwing

test/parallel/test-domain-errors.js

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
'use strict';
2+
3+
/*
4+
* The goal of this test is to make sure that errors thrown within domains
5+
* are handled correctly. It checks that the process 'uncaughtException' event
6+
* is emitted when appropriate, and not emitted when it shouldn't. It also
7+
* checks that the proper domain error handlers are called when they should
8+
* be called, and not called when they shouldn't.
9+
*/
10+
11+
const common = require('../common');
12+
const assert = require('assert');
13+
const domain = require('domain');
14+
const child_process = require('child_process');
15+
16+
const uncaughtExceptions = {};
17+
18+
const tests = [];
19+
20+
function test1() {
21+
/*
22+
* Throwing from an async callback from within a domain that doesn't have
23+
* an error handler must result in emitting the process' uncaughtException
24+
* event.
25+
*/
26+
const d = domain.create();
27+
d.run(function() {
28+
setTimeout(function onTimeout() {
29+
throw new Error('boom!');
30+
});
31+
});
32+
}
33+
34+
tests.push({
35+
fn: test1,
36+
expectedMessages: ['uncaughtException']
37+
});
38+
39+
function test2() {
40+
/*
41+
* Throwing from from within a domain that doesn't have an error handler must
42+
* result in emitting the process' uncaughtException event.
43+
*/
44+
const d2 = domain.create();
45+
d2.run(function() {
46+
throw new Error('boom!');
47+
});
48+
}
49+
50+
tests.push({
51+
fn: test2,
52+
expectedMessages: ['uncaughtException']
53+
});
54+
55+
function test3() {
56+
/*
57+
* This test creates two nested domains: d3 and d4. d4 doesn't register an
58+
* error handler, but d3 does. The error is handled by the d3 domain and thus
59+
* and 'uncaughtException' event should _not_ be emitted.
60+
*/
61+
const d3 = domain.create();
62+
const d4 = domain.create();
63+
64+
d3.on('error', function onErrorInD3Domain(err) {
65+
process.send('errorHandledByDomain');
66+
});
67+
68+
d3.run(function() {
69+
d4.run(function() {
70+
throw new Error('boom!');
71+
});
72+
});
73+
}
74+
75+
tests.push({
76+
fn: test3,
77+
expectedMessages: ['errorHandledByDomain']
78+
});
79+
80+
function test4() {
81+
/*
82+
* This test creates two nested domains: d5 and d6. d6 doesn't register an
83+
* error handler. When the timer's callback is called, because async
84+
* operations like timer callbacks are bound to the domain that was active
85+
* at the time of their creation, and because both d5 and d6 domains have
86+
* exited by the time the timer's callback is called, its callback runs with
87+
* only d6 on the domains stack. Since d6 doesn't register an error handler,
88+
* the process' uncaughtException event should be emitted.
89+
*/
90+
const d5 = domain.create();
91+
const d6 = domain.create();
92+
93+
d5.on('error', function onErrorInD2Domain(err) {
94+
process.send('errorHandledByDomain');
95+
});
96+
97+
d5.run(function() {
98+
d6.run(function() {
99+
setTimeout(function onTimeout() {
100+
throw new Error('boom!');
101+
});
102+
});
103+
});
104+
}
105+
106+
tests.push({
107+
fn: test4,
108+
expectedMessages: ['uncaughtException']
109+
});
110+
111+
function test5() {
112+
/*
113+
* This test creates two nested domains: d7 and d4. d8 _does_ register an
114+
* error handler, so throwing within that domain should not emit an uncaught
115+
* exception.
116+
*/
117+
const d7 = domain.create();
118+
const d8 = domain.create();
119+
120+
d8.on('error', function onErrorInD3Domain(err) {
121+
process.send('errorHandledByDomain');
122+
});
123+
124+
d7.run(function() {
125+
d8.run(function() {
126+
throw new Error('boom!');
127+
});
128+
});
129+
}
130+
tests.push({
131+
fn: test5,
132+
expectedMessages: ['errorHandledByDomain']
133+
});
134+
135+
function test6() {
136+
/*
137+
* This test creates two nested domains: d9 and d10. d10 _does_ register an
138+
* error handler, so throwing within that domain in an async callback should
139+
* _not_ emit an uncaught exception.
140+
*/
141+
const d9 = domain.create();
142+
const d10 = domain.create();
143+
144+
d10.on('error', function onErrorInD2Domain(err) {
145+
process.send('errorHandledByDomain');
146+
});
147+
148+
d9.run(function() {
149+
d10.run(function() {
150+
setTimeout(function onTimeout() {
151+
throw new Error('boom!');
152+
});
153+
});
154+
});
155+
}
156+
157+
tests.push({
158+
fn: test6,
159+
expectedMessages: ['errorHandledByDomain']
160+
});
161+
162+
if (process.argv[2] === 'child') {
163+
const testIndex = process.argv[3];
164+
process.on('uncaughtException', function onUncaughtException() {
165+
process.send('uncaughtException');
166+
});
167+
168+
tests[testIndex].fn();
169+
} else {
170+
// Run each test's function in a child process. Listen on
171+
// messages sent by each child process and compare expected
172+
// messages defined for each ttest from the actual received messages.
173+
tests.forEach(function doTest(test, testIndex) {
174+
const testProcess = child_process.fork(__filename, ['child', testIndex]);
175+
176+
testProcess.on('message', function onMsg(msg) {
177+
if (test.messagesReceived === undefined)
178+
test.messagesReceived = [];
179+
180+
test.messagesReceived.push(msg);
181+
});
182+
183+
testProcess.on('exit', function onExit() {
184+
// Make sure that all expected messages were sent from the
185+
// child process
186+
test.expectedMessages.forEach(function(expectedMessage) {
187+
if (test.messagesReceived === undefined ||
188+
test.messagesReceived.indexOf(expectedMessage) === -1)
189+
assert(false, 'test ' + test.fn.name + ' should have sent message: '
190+
+ expectedMessage + ' but didn\'t');
191+
});
192+
193+
if (test.messagesReceived) {
194+
test.messagesReceived.forEach(function(receivedMessage) {
195+
if (test.expectedMessages.indexOf(receivedMessage) === -1) {
196+
assert(false, 'test ' + test.fn.name +
197+
' should have sent message: ' + receivedMessage + ' but did');
198+
}
199+
});
200+
}
201+
});
202+
});
203+
}

0 commit comments

Comments
 (0)