@@ -193,7 +193,7 @@ static std::string GetErrorSource(Isolate* isolate,
193193}
194194
195195static std::atomic<bool > is_in_oom{false };
196- static std::atomic<bool > is_retrieving_js_stacktrace{false };
196+ static thread_local std::atomic<bool > is_retrieving_js_stacktrace{false };
197197MaybeLocal<StackTrace> GetCurrentStackTrace (Isolate* isolate, int frame_count) {
198198 if (isolate == nullptr ) {
199199 return MaybeLocal<StackTrace>();
@@ -221,9 +221,6 @@ MaybeLocal<StackTrace> GetCurrentStackTrace(Isolate* isolate, int frame_count) {
221221 StackTrace::CurrentStackTrace (isolate, frame_count, options);
222222
223223 is_retrieving_js_stacktrace.store (false );
224- if (stack->GetFrameCount () == 0 ) {
225- return MaybeLocal<StackTrace>();
226- }
227224
228225 return scope.Escape (stack);
229226}
@@ -298,7 +295,8 @@ void PrintStackTrace(Isolate* isolate,
298295
299296void PrintCurrentStackTrace (Isolate* isolate, StackTracePrefix prefix) {
300297 Local<StackTrace> stack;
301- if (GetCurrentStackTrace (isolate).ToLocal (&stack)) {
298+ if (GetCurrentStackTrace (isolate).ToLocal (&stack) &&
299+ stack->GetFrameCount () > 0 ) {
302300 PrintStackTrace (isolate, stack, prefix);
303301 }
304302}
@@ -669,13 +667,52 @@ v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(
669667 };
670668}
671669
670+ // Check if an exception is a stack overflow error (RangeError with
671+ // "Maximum call stack size exceeded" message). This is used to handle
672+ // stack overflow specially in TryCatchScope - instead of immediately
673+ // exiting, we can use the red zone to re-throw to user code.
674+ static bool IsStackOverflowError (Isolate* isolate, Local<Value> exception) {
675+ if (!exception->IsNativeError ()) return false ;
676+
677+ Local<Object> err_obj = exception.As <Object>();
678+ Local<String> constructor_name = err_obj->GetConstructorName ();
679+
680+ // Must be a RangeError
681+ Utf8Value name (isolate, constructor_name);
682+ if (name.ToStringView () != " RangeError" ) return false ;
683+
684+ // Check for the specific stack overflow message
685+ Local<Context> context = isolate->GetCurrentContext ();
686+ Local<Value> message_val;
687+ if (!err_obj->Get (context, String::NewFromUtf8Literal (isolate, " message" ))
688+ .ToLocal (&message_val)) {
689+ return false ;
690+ }
691+
692+ if (!message_val->IsString ()) return false ;
693+
694+ Utf8Value message (isolate, message_val.As <String>());
695+ return message.ToStringView () == " Maximum call stack size exceeded" ;
696+ }
697+
672698namespace errors {
673699
674700TryCatchScope::~TryCatchScope () {
675- if (HasCaught () && !HasTerminated () && mode_ == CatchMode::kFatal ) {
701+ if (HasCaught () && !HasTerminated () && mode_ != CatchMode::kNormal ) {
676702 HandleScope scope (env_->isolate ());
677703 Local<v8::Value> exception = Exception ();
678704 Local<v8::Message> message = Message ();
705+
706+ // Special handling for stack overflow errors in async_hooks: instead of
707+ // immediately exiting, re-throw the exception. This allows the exception
708+ // to propagate to user code's try-catch blocks.
709+ if (mode_ == CatchMode::kFatalRethrowStackOverflow &&
710+ IsStackOverflowError (env_->isolate (), exception)) {
711+ ReThrow ();
712+ Reset ();
713+ return ;
714+ }
715+
679716 EnhanceFatalException enhance = CanContinue () ?
680717 EnhanceFatalException::kEnhance : EnhanceFatalException::kDontEnhance ;
681718 if (message.IsEmpty ())
@@ -1230,8 +1267,26 @@ void TriggerUncaughtException(Isolate* isolate,
12301267 if (env->can_call_into_js ()) {
12311268 // We do not expect the global uncaught exception itself to throw any more
12321269 // exceptions. If it does, exit the current Node.js instance.
1233- errors::TryCatchScope try_catch (env,
1234- errors::TryCatchScope::CatchMode::kFatal );
1270+ // Special case: if the original error was a stack overflow and calling
1271+ // _fatalException causes another stack overflow, rethrow it to allow
1272+ // user code's try-catch blocks to potentially catch it.
1273+ auto is_stack_overflow = [&] {
1274+ return IsStackOverflowError (env->isolate (), error);
1275+ };
1276+ // Without a JS stack, rethrowing may or may not do anything.
1277+ // TODO(addaleax): In V8, expose a way to check whether there is a JS stack
1278+ // or TryCatch that would capture the rethrown exception.
1279+ auto has_js_stack = [&] {
1280+ HandleScope handle_scope (env->isolate ());
1281+ Local<StackTrace> stack;
1282+ return GetCurrentStackTrace (env->isolate (), 1 ).ToLocal (&stack) &&
1283+ stack->GetFrameCount () > 0 ;
1284+ };
1285+ errors::TryCatchScope::CatchMode mode =
1286+ is_stack_overflow () && has_js_stack ()
1287+ ? errors::TryCatchScope::CatchMode::kFatalRethrowStackOverflow
1288+ : errors::TryCatchScope::CatchMode::kFatal ;
1289+ errors::TryCatchScope try_catch (env, mode);
12351290 // Explicitly disable verbose exception reporting -
12361291 // if process._fatalException() throws an error, we don't want it to
12371292 // trigger the per-isolate message listener which will call this
0 commit comments