@@ -194,7 +194,7 @@ static std::string GetErrorSource(Isolate* isolate,
194194}
195195
196196static std::atomic<bool > is_in_oom{false };
197- static std::atomic<bool > is_retrieving_js_stacktrace{false };
197+ static thread_local std::atomic<bool > is_retrieving_js_stacktrace{false };
198198MaybeLocal<StackTrace> GetCurrentStackTrace (Isolate* isolate, int frame_count) {
199199 if (isolate == nullptr ) {
200200 return MaybeLocal<StackTrace>();
@@ -222,9 +222,6 @@ MaybeLocal<StackTrace> GetCurrentStackTrace(Isolate* isolate, int frame_count) {
222222 StackTrace::CurrentStackTrace (isolate, frame_count, options);
223223
224224 is_retrieving_js_stacktrace.store (false );
225- if (stack->GetFrameCount () == 0 ) {
226- return MaybeLocal<StackTrace>();
227- }
228225
229226 return scope.Escape (stack);
230227}
@@ -299,7 +296,8 @@ void PrintStackTrace(Isolate* isolate,
299296
300297void PrintCurrentStackTrace (Isolate* isolate, StackTracePrefix prefix) {
301298 Local<StackTrace> stack;
302- if (GetCurrentStackTrace (isolate).ToLocal (&stack)) {
299+ if (GetCurrentStackTrace (isolate).ToLocal (&stack) &&
300+ stack->GetFrameCount () > 0 ) {
303301 PrintStackTrace (isolate, stack, prefix);
304302 }
305303}
@@ -671,13 +669,52 @@ v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(
671669 };
672670}
673671
672+ // Check if an exception is a stack overflow error (RangeError with
673+ // "Maximum call stack size exceeded" message). This is used to handle
674+ // stack overflow specially in TryCatchScope - instead of immediately
675+ // exiting, we can use the red zone to re-throw to user code.
676+ static bool IsStackOverflowError (Isolate* isolate, Local<Value> exception) {
677+ if (!exception->IsNativeError ()) return false ;
678+
679+ Local<Object> err_obj = exception.As <Object>();
680+ Local<String> constructor_name = err_obj->GetConstructorName ();
681+
682+ // Must be a RangeError
683+ Utf8Value name (isolate, constructor_name);
684+ if (name.ToStringView () != " RangeError" ) return false ;
685+
686+ // Check for the specific stack overflow message
687+ Local<Context> context = isolate->GetCurrentContext ();
688+ Local<Value> message_val;
689+ if (!err_obj->Get (context, String::NewFromUtf8Literal (isolate, " message" ))
690+ .ToLocal (&message_val)) {
691+ return false ;
692+ }
693+
694+ if (!message_val->IsString ()) return false ;
695+
696+ Utf8Value message (isolate, message_val.As <String>());
697+ return message.ToStringView () == " Maximum call stack size exceeded" ;
698+ }
699+
674700namespace errors {
675701
676702TryCatchScope::~TryCatchScope () {
677- if (HasCaught () && !HasTerminated () && mode_ == CatchMode::kFatal ) {
703+ if (HasCaught () && !HasTerminated () && mode_ != CatchMode::kNormal ) {
678704 HandleScope scope (env_->isolate ());
679705 Local<v8::Value> exception = Exception ();
680706 Local<v8::Message> message = Message ();
707+
708+ // Special handling for stack overflow errors in async_hooks: instead of
709+ // immediately exiting, re-throw the exception. This allows the exception
710+ // to propagate to user code's try-catch blocks.
711+ if (mode_ == CatchMode::kFatalRethrowStackOverflow &&
712+ IsStackOverflowError (env_->isolate (), exception)) {
713+ ReThrow ();
714+ Reset ();
715+ return ;
716+ }
717+
681718 EnhanceFatalException enhance = CanContinue () ?
682719 EnhanceFatalException::kEnhance : EnhanceFatalException::kDontEnhance ;
683720 if (message.IsEmpty ())
@@ -1278,8 +1315,26 @@ void TriggerUncaughtException(Isolate* isolate,
12781315 if (env->can_call_into_js ()) {
12791316 // We do not expect the global uncaught exception itself to throw any more
12801317 // exceptions. If it does, exit the current Node.js instance.
1281- errors::TryCatchScope try_catch (env,
1282- errors::TryCatchScope::CatchMode::kFatal );
1318+ // Special case: if the original error was a stack overflow and calling
1319+ // _fatalException causes another stack overflow, rethrow it to allow
1320+ // user code's try-catch blocks to potentially catch it.
1321+ auto is_stack_overflow = [&] {
1322+ return IsStackOverflowError (env->isolate (), error);
1323+ };
1324+ // Without a JS stack, rethrowing may or may not do anything.
1325+ // TODO(addaleax): In V8, expose a way to check whether there is a JS stack
1326+ // or TryCatch that would capture the rethrown exception.
1327+ auto has_js_stack = [&] {
1328+ HandleScope handle_scope (env->isolate ());
1329+ Local<StackTrace> stack;
1330+ return GetCurrentStackTrace (env->isolate (), 1 ).ToLocal (&stack) &&
1331+ stack->GetFrameCount () > 0 ;
1332+ };
1333+ errors::TryCatchScope::CatchMode mode =
1334+ is_stack_overflow () && has_js_stack ()
1335+ ? errors::TryCatchScope::CatchMode::kFatalRethrowStackOverflow
1336+ : errors::TryCatchScope::CatchMode::kFatal ;
1337+ errors::TryCatchScope try_catch (env, mode);
12831338 // Explicitly disable verbose exception reporting -
12841339 // if process._fatalException() throws an error, we don't want it to
12851340 // trigger the per-isolate message listener which will call this
0 commit comments