1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-25 15:29:32 -05:00

feat(core): show unresolved promise origin (#16650)

This commit updates unhelpful messages that are raised when event loop
stalls on unresolved top-level promises.

Instead of "Module evaluation is still pending but there are no pending
ops or dynamic imports. This situation is often caused by unresolved
promises." and "Dynamically imported module evaluation is still pending
but there are no pending ops. This situation is often caused by
unresolved promises." we are now printing a message like: 

error: Top-level await promise never resolved
[SOURCE LINE]
^
    at [FUNCTION NAME] ([FILENAME])

eg:
error: Top-level await promise never resolved
await new Promise((_resolve, _reject) => {});
^
at <anonymous>
(file:///Users/ib/dev/deno/cli/tests/testdata/test/unresolved_promise.ts:1:1)

Co-authored-by: David Sherret <dsherret@users.noreply.github.com>
This commit is contained in:
Bartek Iwańczuk 2022-11-28 23:07:23 +01:00 committed by GitHub
parent fd51b2e506
commit f526513d74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 180 additions and 28 deletions

View file

@ -1,2 +1,5 @@
Check [WILDCARD]/bench/unresolved_promise.ts Check [WILDCARD]/bench/unresolved_promise.ts
error: Module evaluation is still pending but there are no pending ops or dynamic imports. This situation is often caused by unresolved promises. error: Top-level await promise never resolved
await new Promise((_resolve, _reject) => {});
^
at <anonymous> ([WILDCARD]bench/unresolved_promise.ts:1:1)

View file

@ -4,6 +4,7 @@ timeout loop 2
timeout loop 3 timeout loop 3
timeout loop 4 timeout loop 4
timeout loop 5 timeout loop 5
error: Dynamically imported module evaluation is still pending but there are no pending ops. This situation is often caused by unresolved promises. error: Top-level await promise never resolved
Pending dynamic modules: const mod = await import("./tla3/b.js");
- [WILDCARD]/tla3/b.js ^
at <anonymous> ([WILDCARD]/top_level_await/circular.js:5:13)

View file

@ -1 +1,4 @@
error: Module evaluation is still pending but there are no pending ops or dynamic imports. This situation is often caused by unresolved promises. error: Top-level await promise never resolved
await new Promise(() => {});
^
at <anonymous> ([WILDCARD]top_level_await/unresolved.js:1:1)

View file

@ -1,5 +1,10 @@
Check [WILDCARD]/test/unresolved_promise.ts [WILDCARD]
./test/unresolved_promise.ts (uncaught error)
ok | 0 passed | 0 failed ([WILDCARD]) error: Top-level await promise never resolved
await new Promise((_resolve, _reject) => {});
error: Module evaluation is still pending but there are no pending ops or dynamic imports. This situation is often caused by unresolved promises. ^
at <anonymous> ([WILDCARD]/test/unresolved_promise.ts:1:1)
This error was not caught from a test and caused the test runner to fail on the referenced module.
It most likely originated from a dangling promise, event/timeout handler or top-level code.
[WILDCARD]
error: Test failed

View file

@ -207,6 +207,84 @@ impl JsError {
Self::inner_from_v8_exception(scope, exception, Default::default()) Self::inner_from_v8_exception(scope, exception, Default::default())
} }
pub fn from_v8_message<'a>(
scope: &'a mut v8::HandleScope,
msg: v8::Local<'a, v8::Message>,
) -> Self {
// Create a new HandleScope because we're creating a lot of new local
// handles below.
let scope = &mut v8::HandleScope::new(scope);
let exception_message = msg.get(scope).to_rust_string_lossy(scope);
let state_rc = JsRuntime::state(scope);
// Convert them into Vec<JsStackFrame>
let mut frames: Vec<JsStackFrame> = vec![];
let mut source_line = None;
let mut source_line_frame_index = None;
{
let state = &mut *state_rc.borrow_mut();
let script_resource_name = msg
.get_script_resource_name(scope)
.and_then(|v| v8::Local::<v8::String>::try_from(v).ok())
.map(|v| v.to_rust_string_lossy(scope));
let line_number: Option<i64> =
msg.get_line_number(scope).and_then(|v| v.try_into().ok());
let column_number: Option<i64> = msg.get_start_column().try_into().ok();
if let (Some(f), Some(l), Some(c)) =
(script_resource_name, line_number, column_number)
{
// V8's column numbers are 0-based, we want 1-based.
let c = c + 1;
if let Some(source_map_getter) = &state.source_map_getter {
let (f, l, c) = apply_source_map(
f,
l,
c,
&mut state.source_map_cache,
source_map_getter.as_ref(),
);
frames = vec![JsStackFrame::from_location(Some(f), Some(l), Some(c))];
} else {
frames = vec![JsStackFrame::from_location(Some(f), Some(l), Some(c))];
}
}
if let Some(source_map_getter) = &state.source_map_getter {
for (i, frame) in frames.iter().enumerate() {
if let (Some(file_name), Some(line_number)) =
(&frame.file_name, frame.line_number)
{
if !file_name.trim_start_matches('[').starts_with("deno:") {
source_line = get_source_line(
file_name,
line_number,
&mut state.source_map_cache,
source_map_getter.as_ref(),
);
source_line_frame_index = Some(i);
break;
}
}
}
}
}
Self {
name: None,
message: None,
exception_message,
cause: None,
source_line,
source_line_frame_index,
frames,
stack: None,
aggregated: None,
}
}
fn inner_from_v8_exception<'a>( fn inner_from_v8_exception<'a>(
scope: &'a mut v8::HandleScope, scope: &'a mut v8::HandleScope,
exception: v8::Local<'a, v8::Value>, exception: v8::Local<'a, v8::Value>,
@ -330,7 +408,6 @@ impl JsError {
(&frame.file_name, frame.line_number) (&frame.file_name, frame.line_number)
{ {
if !file_name.trim_start_matches('[').starts_with("deno:") { if !file_name.trim_start_matches('[').starts_with("deno:") {
// Source lookup expects a 0-based line number, ours are 1-based.
source_line = get_source_line( source_line = get_source_line(
file_name, file_name,
line_number, line_number,

View file

@ -754,8 +754,8 @@ pub(crate) enum ModuleError {
pub(crate) struct ModuleMap { pub(crate) struct ModuleMap {
// Handling of specifiers and v8 objects // Handling of specifiers and v8 objects
ids_by_handle: HashMap<v8::Global<v8::Module>, ModuleId>, ids_by_handle: HashMap<v8::Global<v8::Module>, ModuleId>,
handles_by_id: HashMap<ModuleId, v8::Global<v8::Module>>, pub handles_by_id: HashMap<ModuleId, v8::Global<v8::Module>>,
info: HashMap<ModuleId, ModuleInfo>, pub info: HashMap<ModuleId, ModuleInfo>,
by_name: HashMap<(String, AssertedModuleType), SymbolicModule>, by_name: HashMap<(String, AssertedModuleType), SymbolicModule>,
next_module_id: ModuleId, next_module_id: ModuleId,
next_load_id: ModuleLoadId, next_load_id: ModuleLoadId,

View file

@ -1168,7 +1168,7 @@ impl JsRuntime {
return Poll::Ready(Ok(())); return Poll::Ready(Ok(()));
} }
let mut state = self.state.borrow_mut(); let state = self.state.borrow();
// Check if more async ops have been dispatched // Check if more async ops have been dispatched
// during this turn of event loop. // during this turn of event loop.
@ -1185,6 +1185,8 @@ impl JsRuntime {
state.waker.wake(); state.waker.wake();
} }
drop(state);
if pending_state.has_pending_module_evaluation { if pending_state.has_pending_module_evaluation {
if pending_state.has_pending_refed_ops if pending_state.has_pending_refed_ops
|| pending_state.has_pending_dyn_imports || pending_state.has_pending_dyn_imports
@ -1195,8 +1197,16 @@ impl JsRuntime {
{ {
// pass, will be polled again // pass, will be polled again
} else { } else {
let msg = "Module evaluation is still pending but there are no pending ops or dynamic imports. This situation is often caused by unresolved promises."; let scope = &mut self.handle_scope();
return Poll::Ready(Err(generic_error(msg))); let messages = find_stalled_top_level_await(scope);
// We are gonna print only a single message to provide a nice formatting
// with source line of offending promise shown. Once user fixed it, then
// they will get another error message for the next promise (but this
// situation is gonna be very rare, if ever happening).
assert!(!messages.is_empty());
let msg = v8::Local::new(scope, messages[0].clone());
let js_error = JsError::from_v8_message(scope, msg);
return Poll::Ready(Err(js_error.into()));
} }
} }
@ -1207,19 +1217,19 @@ impl JsRuntime {
|| pending_state.has_tick_scheduled || pending_state.has_tick_scheduled
{ {
// pass, will be polled again // pass, will be polled again
} else if state.dyn_module_evaluate_idle_counter >= 1 { } else if self.state.borrow().dyn_module_evaluate_idle_counter >= 1 {
let mut msg = "Dynamically imported module evaluation is still pending but there are no pending ops. This situation is often caused by unresolved promises. let scope = &mut self.handle_scope();
Pending dynamic modules:\n".to_string(); let messages = find_stalled_top_level_await(scope);
let module_map = self.module_map.as_mut().unwrap().borrow_mut(); // We are gonna print only a single message to provide a nice formatting
for pending_evaluate in &state.pending_dyn_mod_evaluate { // with source line of offending promise shown. Once user fixed it, then
let module_info = module_map // they will get another error message for the next promise (but this
.get_info_by_id(&pending_evaluate.module_id) // situation is gonna be very rare, if ever happening).
.unwrap(); assert!(!messages.is_empty());
msg.push_str("- "); let msg = v8::Local::new(scope, messages[0].clone());
msg.push_str(module_info.name.as_str()); let js_error = JsError::from_v8_message(scope, msg);
} return Poll::Ready(Err(js_error.into()));
return Poll::Ready(Err(generic_error(msg)));
} else { } else {
let mut state = self.state.borrow_mut();
// Delay the above error by one spin of the event loop. A dynamic import // Delay the above error by one spin of the event loop. A dynamic import
// evaluation may complete during this, in which case the counter will // evaluation may complete during this, in which case the counter will
// reset. // reset.
@ -1269,6 +1279,59 @@ Pending dynamic modules:\n".to_string();
} }
} }
fn get_stalled_top_level_await_message_for_module<'s>(
scope: &'s mut v8::HandleScope,
module_id: ModuleId,
) -> Vec<v8::Global<v8::Message>> {
let module_map = JsRuntime::module_map(scope);
let module_map = module_map.borrow();
let module_handle = module_map.handles_by_id.get(&module_id).unwrap();
let module = v8::Local::new(scope, module_handle);
let stalled = module.get_stalled_top_level_await_message(scope);
let mut messages = vec![];
for (_, message) in stalled {
messages.push(v8::Global::new(scope, message));
}
messages
}
fn find_stalled_top_level_await<'s>(
scope: &'s mut v8::HandleScope,
) -> Vec<v8::Global<v8::Message>> {
let module_map = JsRuntime::module_map(scope);
let module_map = module_map.borrow();
// First check if that's root module
let root_module_id = module_map
.info
.values()
.filter(|m| m.main)
.map(|m| m.id)
.next();
if let Some(root_module_id) = root_module_id {
let messages =
get_stalled_top_level_await_message_for_module(scope, root_module_id);
if !messages.is_empty() {
return messages;
}
}
// It wasn't a top module, so iterate over all modules and try to find
// any with stalled top level await
let module_ids = module_map.handles_by_id.keys().copied().collect::<Vec<_>>();
for module_id in module_ids {
let messages =
get_stalled_top_level_await_message_for_module(scope, module_id);
if !messages.is_empty() {
return messages;
}
}
unreachable!()
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct EventLoopPendingState { pub(crate) struct EventLoopPendingState {
has_pending_refed_ops: bool, has_pending_refed_ops: bool,