Skip to content

Commit 4401bb4

Browse files
committed
domain: Do not use uncaughtException handler
This adds a process._fatalException method which is called into from C++ in order to either emit the 'uncaughtException' method, or emit 'error' on the active domain. The 'uncaughtException' event is an implementation detail that it would be nice to deprecate one day, so exposing it as part of the domain machinery is not ideal. Fix #4375
1 parent c6e958d commit 4401bb4

5 files changed

Lines changed: 129 additions & 102 deletions

File tree

lib/domain.js

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,27 +46,6 @@ exports._stack = stack;
4646
exports.active = null;
4747

4848

49-
// loading this file the first time sets up the global
50-
// uncaughtException handler.
51-
process.on('uncaughtException', uncaughtHandler);
52-
53-
function uncaughtHandler(er) {
54-
// if there's an active domain, then handle this there.
55-
// Note that if this error emission throws, then it'll just crash.
56-
if (exports.active && !exports.active._disposed) {
57-
util._extend(er, {
58-
domain: exports.active,
59-
domain_thrown: true
60-
});
61-
exports.active.emit('error', er);
62-
if (exports.active) exports.active.exit();
63-
} else if (process.listeners('uncaughtException').length === 1) {
64-
// if there are other handlers, then they'll take care of it.
65-
// but if not, then we need to crash now.
66-
throw er;
67-
}
68-
}
69-
7049
inherits(Domain, EventEmitter);
7150

7251
function Domain() {

lib/timers.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,23 +86,28 @@ function insert(item, msecs) {
8686

8787
if (!first._onTimeout) continue;
8888

89-
// v0.4 compatibility: if the timer callback throws and the user's
90-
// uncaughtException handler ignores the exception, other timers that
91-
// expire on this tick should still run. If #2582 goes through, this
92-
// hack should be removed.
89+
// v0.4 compatibility: if the timer callback throws and the
90+
// domain or uncaughtException handler ignore the exception,
91+
// other timers that expire on this tick should still run.
9392
//
9493
// https://github.com/joyent/node/issues/2631
95-
if (first.domain) {
96-
if (first.domain._disposed) continue;
97-
first.domain.enter();
98-
}
94+
var domain = first.domain;
95+
if (domain && domain._disposed) continue;
9996
try {
97+
if (domain)
98+
domain.enter();
99+
var threw = true;
100100
first._onTimeout();
101-
} catch (e) {
102-
if (!process.listeners('uncaughtException').length) throw e;
103-
process.emit('uncaughtException', e);
101+
if (domain)
102+
domain.exit();
103+
threw = false;
104+
} finally {
105+
if (threw) {
106+
process.nextTick(function() {
107+
list.ontimeout();
108+
});
109+
}
104110
}
105-
if (first.domain) first.domain.exit();
106111
}
107112
}
108113

src/node.cc

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,7 @@ static Persistent<String> rss_symbol;
110110
static Persistent<String> heap_total_symbol;
111111
static Persistent<String> heap_used_symbol;
112112

113-
static Persistent<String> listeners_symbol;
114-
static Persistent<String> uncaught_exception_symbol;
115-
static Persistent<String> emit_symbol;
113+
static Persistent<String> fatal_exception_symbol;
116114

117115
static Persistent<Function> process_makeCallback;
118116

@@ -1883,47 +1881,36 @@ static void OnFatalError(const char* location, const char* message) {
18831881
void FatalException(TryCatch &try_catch) {
18841882
HandleScope scope;
18851883

1886-
if (listeners_symbol.IsEmpty()) {
1887-
listeners_symbol = NODE_PSYMBOL("listeners");
1888-
uncaught_exception_symbol = NODE_PSYMBOL("uncaughtException");
1889-
emit_symbol = NODE_PSYMBOL("emit");
1890-
}
1891-
1892-
Local<Value> listeners_v = process->Get(listeners_symbol);
1893-
assert(listeners_v->IsFunction());
1894-
1895-
Local<Function> listeners = Local<Function>::Cast(listeners_v);
1896-
1897-
Local<String> uncaught_exception_symbol_l = Local<String>::New(uncaught_exception_symbol);
1898-
Local<Value> argv[1] = { uncaught_exception_symbol_l };
1899-
Local<Value> ret = listeners->Call(process, 1, argv);
1884+
if (fatal_exception_symbol.IsEmpty())
1885+
fatal_exception_symbol = NODE_PSYMBOL("_fatalException");
19001886

1901-
assert(ret->IsArray());
1887+
Local<Value> fatal_v = process->Get(fatal_exception_symbol);
19021888

1903-
Local<Array> listener_array = Local<Array>::Cast(ret);
1904-
1905-
uint32_t length = listener_array->Length();
1906-
// Report and exit if process has no "uncaughtException" listener
1907-
if (length == 0) {
1889+
if (!fatal_v->IsFunction()) {
1890+
// failed before the process._fatalException function was added!
1891+
// this is probably pretty bad. Nothing to do but report and exit.
19081892
ReportException(try_catch, true);
19091893
exit(1);
19101894
}
19111895

1912-
// Otherwise fire the process "uncaughtException" event
1913-
Local<Value> emit_v = process->Get(emit_symbol);
1914-
assert(emit_v->IsFunction());
1915-
1916-
Local<Function> emit = Local<Function>::Cast(emit_v);
1896+
Local<Function> fatal_f = Local<Function>::Cast(fatal_v);
19171897

19181898
Local<Value> error = try_catch.Exception();
1919-
Local<Value> event_argv[2] = { uncaught_exception_symbol_l, error };
1899+
Local<Value> argv[] = { error };
1900+
1901+
TryCatch fatal_try_catch;
1902+
1903+
// this will return true if the JS layer handled it, false otherwise
1904+
Local<Value> caught = fatal_f->Call(process, ARRAY_SIZE(argv), argv);
19201905

1921-
TryCatch event_try_catch;
1922-
emit->Call(process, 2, event_argv);
1906+
if (fatal_try_catch.HasCaught()) {
1907+
// the fatal exception function threw, so we must exit
1908+
ReportException(fatal_try_catch, true);
1909+
exit(1);
1910+
}
19231911

1924-
if (event_try_catch.HasCaught()) {
1925-
// the uncaught exception event threw, so we must exit.
1926-
ReportException(event_try_catch, true);
1912+
if (false == caught->BooleanValue()) {
1913+
ReportException(try_catch, true);
19271914
exit(1);
19281915
}
19291916
}

src/node.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838

3939
process.EventEmitter = EventEmitter; // process.EventEmitter is deprecated
4040

41+
// do this good and early, since it handles errors.
42+
startup.processFatal();
43+
4144
startup.globalVariables();
4245
startup.globalTimeouts();
4346
startup.globalConsole();
@@ -211,6 +214,47 @@
211214
return startup._lazyConstants;
212215
};
213216

217+
startup.processFatal = function() {
218+
// call into the active domain, or emit uncaughtException,
219+
// and exit if there are no listeners.
220+
process._fatalException = function(er) {
221+
var caught = false;
222+
if (process.domain) {
223+
var domain = process.domain;
224+
225+
// ignore errors on disposed domains.
226+
//
227+
// XXX This is a bit stupid. We should probably get rid of
228+
// domain.dispose() altogether. It's almost always a terrible
229+
// idea. --isaacs
230+
if (domain._disposed)
231+
return true;
232+
233+
er.domain = domain;
234+
er.domain_thrown = true;
235+
// wrap this in a try/catch so we don't get infinite throwing
236+
try {
237+
// One of three things will happen here.
238+
//
239+
// 1. There is a handler, caught = true
240+
// 2. There is no handler, caught = false
241+
// 3. It throws, caught = false
242+
//
243+
// If caught is false after this, then there's no need to exit()
244+
// the domain, because we're going to crash the process anyway.
245+
caught = domain.emit('error', er);
246+
domain.exit();
247+
} catch (er2) {
248+
caught = false;
249+
}
250+
} else {
251+
caught = process.emit('uncaughtException', er);
252+
}
253+
// if someone handled it, then great. otherwise, die in C++ land
254+
return caught;
255+
};
256+
};
257+
214258
var assert;
215259
startup.processAssert = function() {
216260
// Note that calls to assert() are pre-processed out by JS2C for the

test/simple/test-domain.js

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ var common = require('../common');
2626
var assert = require('assert');
2727
var domain = require('domain');
2828
var events = require('events');
29+
var fs = require('fs');
2930
var caught = 0;
30-
var expectCaught = 8;
31+
var expectCaught = 0;
3132

3233
var d = new domain.Domain();
3334
var e = new events.EventEmitter();
3435

3536
d.on('error', function(er) {
36-
console.error('caught', er);
37+
console.error('caught', er && (er.message || er));
3738

3839
var er_message = er.message;
3940
var er_path = er.path
@@ -110,24 +111,58 @@ d.on('error', function(er) {
110111
break;
111112

112113
default:
113-
console.error('unexpected error, throwing %j', er.message);
114+
console.error('unexpected error, throwing %j', er.message || er);
114115
throw er;
115116
}
116117

117118
caught++;
118119
});
119120

121+
122+
120123
process.on('exit', function() {
121-
console.error('exit');
122-
assert.equal(caught, expectCaught);
124+
console.error('exit', caught, expectCaught);
125+
assert.equal(caught, expectCaught, 'caught the expected number of errors');
123126
console.log('ok');
124127
});
125128

126129

130+
// catch thrown errors no matter how many times we enter the event loop
131+
// this only uses implicit binding, except for the first function
132+
// passed to d.run(). The rest are implicitly bound by virtue of being
133+
// set up while in the scope of the d domain.
134+
d.run(function() {
135+
process.nextTick(function() {
136+
var i = setInterval(function () {
137+
clearInterval(i);
138+
setTimeout(function() {
139+
fs.stat('this file does not exist', function(er, stat) {
140+
// uh oh! stat isn't set!
141+
// pretty common error.
142+
console.log(stat.isDirectory());
143+
});
144+
});
145+
});
146+
});
147+
});
148+
expectCaught++;
149+
150+
151+
152+
// implicit addition of a timer created within a domain-bound context.
153+
d.run(function() {
154+
setTimeout(function() {
155+
throw new Error('implicit timer');
156+
});
157+
});
158+
expectCaught++;
159+
160+
127161

128162
// Event emitters added to the domain have their errors routed.
129163
d.add(e);
130164
e.emit('error', new Error('emitted'));
165+
expectCaught++;
131166

132167

133168

@@ -141,6 +176,9 @@ function fn(er) {
141176

142177
var bound = d.intercept(fn);
143178
bound(new Error('bound'));
179+
expectCaught++;
180+
181+
144182

145183
// intercepted should never pass first argument to callback
146184
function fn2(data) {
@@ -169,36 +207,16 @@ function thrower() {
169207
throw new Error('thrown');
170208
}
171209
setTimeout(d.bind(thrower), 100);
210+
expectCaught++;
172211

173212

174213

175214
// Pass an intercepted function to an fs operation that fails.
176-
var fs = require('fs');
177215
fs.open('this file does not exist', 'r', d.intercept(function(er) {
178216
console.error('should not get here!', er);
179217
throw new Error('should not get here!');
180218
}, true));
181-
182-
183-
184-
// catch thrown errors no matter how many times we enter the event loop
185-
// this only uses implicit binding, except for the first function
186-
// passed to d.run(). The rest are implicitly bound by virtue of being
187-
// set up while in the scope of the d domain.
188-
d.run(function() {
189-
process.nextTick(function() {
190-
var i = setInterval(function () {
191-
clearInterval(i);
192-
setTimeout(function() {
193-
fs.stat('this file does not exist', function(er, stat) {
194-
// uh oh! stat isn't set!
195-
// pretty common error.
196-
console.log(stat.isDirectory());
197-
});
198-
});
199-
});
200-
});
201-
});
219+
expectCaught++;
202220

203221

204222

@@ -213,16 +231,9 @@ setTimeout(function() {
213231
// escape from the domain, but implicit is still bound to it.
214232
implicit.emit('error', new Error('implicit'));
215233
}, 10);
234+
expectCaught++;
216235

217236

218-
219-
// implicit addition of a timer created within a domain-bound context.
220-
d.run(function() {
221-
setTimeout(function() {
222-
throw new Error('implicit timer');
223-
});
224-
});
225-
226237
var result = d.run(function () {
227238
return 'return value';
228239
});
@@ -231,3 +242,4 @@ assert.equal(result, 'return value');
231242

232243
var fst = fs.createReadStream('stream for nonexistent file')
233244
d.add(fst)
245+
expectCaught++;

0 commit comments

Comments
 (0)