Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src: refactor uncaught exception handling #28257

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,12 +254,18 @@ Object.defineProperty(process, 'features', {

{
const {
fatalException,
onGlobalUncaughtException,
setUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback
} = require('internal/process/execution');

process._fatalException = fatalException;
// For legacy reasons this is still called `_fatalException`, even
// though it is now a global uncaught exception handler.
// The C++ land node::errors::TriggerUncaughtException grabs it
// from the process object because it has been monkey-patchable.
// TODO(joyeecheung): investigate whether process._fatalException
// can be deprecated.
process._fatalException = onGlobalUncaughtException;
process.setUncaughtExceptionCaptureCallback =
setUncaughtExceptionCaptureCallback;
process.hasUncaughtExceptionCaptureCallback =
Expand Down
23 changes: 13 additions & 10 deletions lib/internal/main/worker_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const {
} = workerIo;

const {
fatalException: originalFatalException
onGlobalUncaughtException
} = require('internal/process/execution');

const publicWorker = require('worker_threads');
Expand Down Expand Up @@ -156,24 +156,23 @@ port.on('message', (message) => {
}
});

// Overwrite fatalException
process._fatalException = (error, fromPromise) => {
debug(`[${threadId}] gets fatal exception`);
let caught = false;
function workerOnGlobalUncaughtException(error, fromPromise) {
debug(`[${threadId}] gets uncaught exception`);
let handled = false;
try {
caught = originalFatalException.call(this, error, fromPromise);
handled = onGlobalUncaughtException(error, fromPromise);
} catch (e) {
error = e;
}
debug(`[${threadId}] fatal exception caught = ${caught}`);
debug(`[${threadId}] uncaught exception handled = ${handled}`);

if (!caught) {
if (!handled) {
let serialized;
try {
const { serializeError } = require('internal/error-serdes');
serialized = serializeError(error);
} catch {}
debug(`[${threadId}] fatal exception serialized = ${!!serialized}`);
debug(`[${threadId}] uncaught exception serialized = ${!!serialized}`);
if (serialized)
port.postMessage({
type: ERROR_MESSAGE,
Expand All @@ -187,7 +186,11 @@ process._fatalException = (error, fromPromise) => {

process.exit();
}
};
}

// Patch the global uncaught exception handler so it gets picked up by
// node::errors::TriggerUncaughtException().
process._fatalException = workerOnGlobalUncaughtException;

markBootstrapComplete();

Expand Down
2 changes: 1 addition & 1 deletion lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,7 @@ Module.runMain = function() {
return loader.import(pathToFileURL(process.argv[1]).pathname);
})
.catch((e) => {
internalBinding('task_queue').triggerFatalException(
internalBinding('errors').triggerUncaughtException(
e,
true /* fromPromise */
);
Expand Down
14 changes: 9 additions & 5 deletions lib/internal/process/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,14 @@ function noop() {}
// and exported to be written to process._fatalException, it has to be
// returned as an *anonymous function* wrapped inside a factory function,
// otherwise it breaks the test-timers.setInterval async hooks test -
// this may indicate that node::FatalException should fix up the callback scope
// before calling into process._fatalException, or this function should
// take extra care of the async hooks before it schedules a setImmediate.
function createFatalException() {
// this may indicate that node::errors::TriggerUncaughtException() should
// fix up the callback scope before calling into process._fatalException,
// or this function should take extra care of the async hooks before it
// schedules a setImmediate.
function createOnGlobalUncaughtException() {
// The C++ land node::errors::TriggerUncaughtException() will
// exit the process if it returns false, and continue execution if it
// returns true (which indicates that the exception is handled by the user).
return (er, fromPromise) => {
// It's possible that defaultTriggerAsyncId was set for a constructor
// call that threw and was never cleared. So clear it now.
Expand Down Expand Up @@ -206,7 +210,7 @@ module.exports = {
tryGetCwd,
evalModule,
evalScript,
fatalException: createFatalException(),
onGlobalUncaughtException: createOnGlobalUncaughtException(),
setUncaughtExceptionCaptureCallback,
hasUncaughtExceptionCaptureCallback
};
48 changes: 24 additions & 24 deletions lib/internal/process/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

const { Object } = primordials;

const {
safeToString
} = internalBinding('util');
const {
tickInfo,
promiseRejectEvents: {
Expand All @@ -13,10 +10,14 @@ const {
kPromiseResolveAfterResolved,
kPromiseRejectAfterResolved
},
setPromiseRejectCallback,
triggerFatalException
setPromiseRejectCallback
} = internalBinding('task_queue');

const {
noSideEffectsToString,
triggerUncaughtException
} = internalBinding('errors');

// *Must* match Environment::TickInfo::Fields in src/env.h.
const kHasRejectionToWarn = 1;

Expand Down Expand Up @@ -127,7 +128,7 @@ function handledRejection(promise) {

const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning';
function emitWarning(uid, reason) {
const warning = getError(
const warning = getErrorWithoutStack(
unhandledRejectionErrName,
'Unhandled promise rejection. This error originated either by ' +
'throwing inside of an async function without a catch block, ' +
Expand All @@ -139,7 +140,8 @@ function emitWarning(uid, reason) {
warning.stack = reason.stack;
process.emitWarning(reason.stack, unhandledRejectionErrName);
} else {
process.emitWarning(safeToString(reason), unhandledRejectionErrName);
process.emitWarning(
noSideEffectsToString(reason), unhandledRejectionErrName);
}
} catch {}

Expand Down Expand Up @@ -184,7 +186,9 @@ function processPromiseRejections() {
const { reason, uid } = promiseInfo;
switch (unhandledRejectionsMode) {
case kThrowUnhandledRejections: {
fatalException(reason);
const err = reason instanceof Error ?
reason : generateUnhandledRejectionError(reason);
triggerUncaughtException(err, true /* fromPromise */);
const handled = process.emit('unhandledRejection', reason, promise);
if (!handled) emitWarning(uid, reason);
break;
Expand All @@ -210,7 +214,7 @@ function processPromiseRejections() {
pendingUnhandledRejections.length !== 0;
}

function getError(name, message) {
function getErrorWithoutStack(name, message) {
// Reset the stack to prevent any overhead.
const tmp = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
Expand All @@ -226,21 +230,17 @@ function getError(name, message) {
return err;
}

function fatalException(reason) {
let err;
if (reason instanceof Error) {
err = reason;
} else {
err = getError(
'UnhandledPromiseRejection',
'This error originated either by ' +
'throwing inside of an async function without a catch block, ' +
'or by rejecting a promise which was not handled with .catch().' +
` The promise rejected with the reason "${safeToString(reason)}".`
);
err.code = 'ERR_UNHANDLED_REJECTION';
}
triggerFatalException(err, true /* fromPromise */);
function generateUnhandledRejectionError(reason) {
const message =
'This error originated either by ' +
'throwing inside of an async function without a catch block, ' +
'or by rejecting a promise which was not handled with .catch().' +
' The promise rejected with the reason ' +
`"${noSideEffectsToString(reason)}".`;

const err = getErrorWithoutStack('UnhandledPromiseRejection', message);
err.code = 'ERR_UNHANDLED_REJECTION';
return err;
}

function listenForRejections() {
Expand Down
15 changes: 8 additions & 7 deletions lib/internal/process/task_queues.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ const {
// Used to run V8's micro task queue.
runMicrotasks,
setTickCallback,
enqueueMicrotask,
triggerFatalException
enqueueMicrotask
} = internalBinding('task_queue');

const {
triggerUncaughtException
} = internalBinding('errors');

const {
setHasRejectionToWarn,
hasRejectionToWarn,
Expand Down Expand Up @@ -143,11 +146,9 @@ function runMicrotask() {
try {
callback();
} catch (error) {
// TODO(devsnek): Remove this if
// https://bugs.chromium.org/p/v8/issues/detail?id=8326
// is resolved such that V8 triggers the fatal exception
// handler for microtasks.
triggerFatalException(error, false /* fromPromise */);
// runInAsyncScope() swallows the error so we need to catch
// it and handle it here.
triggerUncaughtException(error, false /* fromPromise */);
Copy link
Member Author

@joyeecheung joyeecheung Jun 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@devsnek I tried a cctest on V8 master and it seems the issue still exist. However we still need to catch the error even when it's fixed since runInAsyncScope() will swallow the error.

} finally {
this.emitDestroy();
}
Expand Down
15 changes: 5 additions & 10 deletions src/api/exceptions.cc
Original file line number Diff line number Diff line change
Expand Up @@ -244,17 +244,12 @@ Local<Value> WinapiErrnoException(Isolate* isolate,
}
#endif

// Implement the legacy name exposed in node.h. This has not been in fact
// fatal any more, as the user can handle the exception in the
// TryCatch by listening to `uncaughtException`.
// TODO(joyeecheung): deprecate it in favor of a more accurate name.
void FatalException(Isolate* isolate, const v8::TryCatch& try_catch) {
// If we try to print out a termination exception, we'd just get 'null',
// so just crashing here with that information seems like a better idea,
// and in particular it seems like we should handle terminations at the call
// site for this function rather than by printing them out somewhere.
CHECK(!try_catch.HasTerminated());

HandleScope scope(isolate);
if (!try_catch.IsVerbose()) {
FatalException(isolate, try_catch.Exception(), try_catch.Message());
}
errors::TriggerUncaughtException(isolate, try_catch);
}

} // namespace node
2 changes: 1 addition & 1 deletion src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ void Environment::RunAndClearNativeImmediates() {
ref_count++;
if (UNLIKELY(try_catch.HasCaught())) {
if (!try_catch.HasTerminated())
FatalException(isolate(), try_catch);
errors::TriggerUncaughtException(isolate(), try_catch);

// We are done with the current callback. Increase the counter so that
// the steps below make everything *after* the current item part of
Expand Down
10 changes: 5 additions & 5 deletions src/js_stream.cc
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ bool JSStream::IsClosing() {
Local<Value> value;
if (!MakeCallback(env()->isclosing_string(), 0, nullptr).ToLocal(&value)) {
if (try_catch.HasCaught() && !try_catch.HasTerminated())
FatalException(env()->isolate(), try_catch);
errors::TriggerUncaughtException(env()->isolate(), try_catch);
return true;
}
return value->IsTrue();
Expand All @@ -65,7 +65,7 @@ int JSStream::ReadStart() {
if (!MakeCallback(env()->onreadstart_string(), 0, nullptr).ToLocal(&value) ||
!value->Int32Value(env()->context()).To(&value_int)) {
if (try_catch.HasCaught() && !try_catch.HasTerminated())
FatalException(env()->isolate(), try_catch);
errors::TriggerUncaughtException(env()->isolate(), try_catch);
}
return value_int;
}
Expand All @@ -80,7 +80,7 @@ int JSStream::ReadStop() {
if (!MakeCallback(env()->onreadstop_string(), 0, nullptr).ToLocal(&value) ||
!value->Int32Value(env()->context()).To(&value_int)) {
if (try_catch.HasCaught() && !try_catch.HasTerminated())
FatalException(env()->isolate(), try_catch);
errors::TriggerUncaughtException(env()->isolate(), try_catch);
}
return value_int;
}
Expand All @@ -102,7 +102,7 @@ int JSStream::DoShutdown(ShutdownWrap* req_wrap) {
argv).ToLocal(&value) ||
!value->Int32Value(env()->context()).To(&value_int)) {
if (try_catch.HasCaught() && !try_catch.HasTerminated())
FatalException(env()->isolate(), try_catch);
errors::TriggerUncaughtException(env()->isolate(), try_catch);
}
return value_int;
}
Expand Down Expand Up @@ -137,7 +137,7 @@ int JSStream::DoWrite(WriteWrap* w,
argv).ToLocal(&value) ||
!value->Int32Value(env()->context()).To(&value_int)) {
if (try_catch.HasCaught() && !try_catch.HasTerminated())
FatalException(env()->isolate(), try_catch);
errors::TriggerUncaughtException(env()->isolate(), try_catch);
}
return value_int;
}
Expand Down
8 changes: 3 additions & 5 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ using options_parser::kDisallowedInEnvironment;

using v8::Boolean;
using v8::EscapableHandleScope;
using v8::Exception;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::HandleScope;
Expand Down Expand Up @@ -218,10 +217,9 @@ MaybeLocal<Value> ExecuteBootstrapper(Environment* env,
arguments->size(),
arguments->data());

// If there was an error during bootstrap then it was either handled by the
// FatalException handler or it's unrecoverable (e.g. max call stack
// exceeded). Either way, clear the stack so that the AsyncCallbackScope
// destructor doesn't fail on the id check.
// If there was an error during bootstrap, it must be unrecoverable
// (e.g. max call stack exceeded). Clear the stack so that the
// AsyncCallbackScope destructor doesn't fail on the id check.
// There are only two ways to have a stack size > 1: 1) the user manually
// called MakeCallback or 2) user awaited during bootstrap, which triggered
// _tickCallback().
Expand Down
2 changes: 1 addition & 1 deletion src/node_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ static inline void trigger_fatal_exception(
napi_env env, v8::Local<v8::Value> local_err) {
v8::Local<v8::Message> local_msg =
v8::Exception::CreateMessage(env->isolate, local_err);
node::FatalException(env->isolate, local_err, local_msg);
node::errors::TriggerUncaughtException(env->isolate, local_err, local_msg);
}

class ThreadSafeFunction : public node::AsyncResource {
Expand Down
6 changes: 3 additions & 3 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ void ContextifyScript::New(const FunctionCallbackInfo<Value>& args) {
compile_options);

if (v8_script.IsEmpty()) {
DecorateErrorStack(env, try_catch);
errors::DecorateErrorStack(env, try_catch);
no_abort_scope.Close();
if (!try_catch.HasTerminated())
try_catch.ReThrow();
Expand Down Expand Up @@ -946,7 +946,7 @@ bool ContextifyScript::EvalMachine(Environment* env,
if (try_catch.HasCaught()) {
if (!timed_out && !received_signal && display_errors) {
// We should decorate non-termination exceptions
DecorateErrorStack(env, try_catch);
errors::DecorateErrorStack(env, try_catch);
}

// If there was an exception thrown during script execution, re-throw it.
Expand Down Expand Up @@ -1110,7 +1110,7 @@ void ContextifyContext::CompileFunction(

if (maybe_fn.IsEmpty()) {
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
DecorateErrorStack(env, try_catch);
errors::DecorateErrorStack(env, try_catch);
try_catch.ReThrow();
}
return;
Expand Down
Loading