Skip to main content

deno_core/
error.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3use std::borrow::Cow;
4use std::collections::HashSet;
5use std::error::Error;
6use std::fmt;
7use std::fmt::Debug;
8use std::fmt::Display;
9use std::fmt::Formatter;
10use std::fmt::Write as _;
11use std::sync::Arc;
12
13use boxed_error::Boxed;
14use deno_error::JsError;
15use deno_error::JsErrorClass;
16use deno_error::PropertyValue;
17use deno_error::builtin_classes::*;
18use thiserror::Error;
19
20pub use super::modules::ModuleConcreteError;
21use crate::FastStaticString;
22pub use crate::io::ResourceError;
23pub use crate::modules::ModuleLoaderError;
24use crate::runtime::JsRealm;
25use crate::runtime::JsRuntime;
26use crate::runtime::v8_static_strings;
27use crate::source_map::SourceMapApplication;
28use crate::url::Url;
29
30/// A generic wrapper that can encapsulate any concrete error type.
31// TODO(ry) Deprecate AnyError and encourage deno_core::anyhow::Error instead.
32pub type AnyError = anyhow::Error;
33
34deno_error::js_error_wrapper!(v8::DataError, DataError, TYPE_ERROR);
35
36impl PartialEq<DataError> for DataError {
37  fn eq(&self, other: &DataError) -> bool {
38    match (self.0, other.0) {
39      (
40        v8::DataError::BadType { actual, expected },
41        v8::DataError::BadType {
42          actual: other_actual,
43          expected: other_expected,
44        },
45      ) => actual == other_actual && expected == other_expected,
46      (
47        v8::DataError::NoData { expected },
48        v8::DataError::NoData {
49          expected: other_expected,
50        },
51      ) => expected == other_expected,
52      _ => false,
53    }
54  }
55}
56
57impl Eq for DataError {}
58
59#[derive(Debug, Error, JsError)]
60#[class(generic)]
61#[error("Failed to parse {0}")]
62pub struct CoreModuleParseError(pub FastStaticString);
63
64#[derive(Debug, Error, JsError)]
65#[class(generic)]
66#[error("Failed to execute {0}")]
67pub struct CoreModuleExecuteError(pub FastStaticString);
68
69#[derive(Debug, Error, JsError)]
70#[class(generic)]
71#[error("Unable to get code cache from unbound module script for {0}")]
72pub struct CreateCodeCacheError(pub Url);
73
74#[derive(Debug, Error, JsError)]
75#[class(generic)]
76#[error(
77  "Extensions from snapshot loaded in wrong order: expected {} but got {}", .expected, .actual
78)]
79pub struct ExtensionSnapshotMismatchError {
80  pub expected: &'static str,
81  pub actual: &'static str,
82}
83
84#[derive(Debug, Error, JsError)]
85#[class(generic)]
86#[error(
87  "Number of lazy-initialized extensions ({}) does not match number of arguments ({})", .lazy_init_extensions_len, .arguments_len
88)]
89pub struct ExtensionLazyInitCountMismatchError {
90  pub lazy_init_extensions_len: usize,
91  pub arguments_len: usize,
92}
93
94#[derive(Debug, Error, JsError)]
95#[class(generic)]
96#[error(
97  "Lazy-initialized extensions loaded in wrong order: expected {} but got {}", .expected, .actual
98)]
99pub struct ExtensionLazyInitOrderMismatchError {
100  pub expected: &'static str,
101  pub actual: &'static str,
102}
103
104#[derive(Debug, Boxed, JsError)]
105pub struct CoreError(pub Box<CoreErrorKind>);
106
107#[derive(Debug, thiserror::Error, JsError)]
108pub enum CoreErrorKind {
109  #[class(generic)]
110  #[error("Top-level await is not allowed in synchronous evaluation")]
111  TLA,
112  #[class(inherit)]
113  #[error(transparent)]
114  Js(#[from] Box<JsError>),
115  #[class(inherit)]
116  #[error(transparent)]
117  Io(#[from] std::io::Error),
118  #[class(inherit)]
119  #[error(transparent)]
120  ExtensionTranspiler(deno_error::JsErrorBox),
121  #[class(inherit)]
122  #[error(transparent)]
123  Parse(#[from] CoreModuleParseError),
124  #[class(inherit)]
125  #[error(transparent)]
126  Execute(#[from] CoreModuleExecuteError),
127  #[class(generic)]
128  #[error(
129    "Following modules were passed to ExtModuleLoader but never used:\n{}",
130    .0.iter().map(|s| format!("  - {}\n", s)).collect::<Vec<_>>().join("")
131  )]
132  UnusedModules(Vec<String>),
133  #[class(generic)]
134  #[error(
135    "Following modules were not evaluated; make sure they are imported from other code:\n{}",
136    .0.iter().map(|s| format!("  - {}\n", s)).collect::<Vec<_>>().join("")
137  )]
138  NonEvaluatedModules(Vec<String>),
139  #[class(generic)]
140  #[error("{0} not present in the module map")]
141  MissingFromModuleMap(String),
142  #[class(generic)]
143  #[error("Could not execute {specifier}")]
144  CouldNotExecute {
145    #[source]
146    error: Box<Self>,
147    specifier: String,
148  },
149  #[class(inherit)]
150  #[error(transparent)]
151  JsBox(#[from] deno_error::JsErrorBox),
152  #[class(inherit)]
153  #[error(transparent)]
154  Url(#[from] url::ParseError),
155  #[class(generic)]
156  #[error(
157    "Cannot evaluate module, because JavaScript execution has been terminated"
158  )]
159  ExecutionTerminated,
160  #[class(generic)]
161  #[error(
162    "Promise resolution is still pending but the event loop has already resolved"
163  )]
164  PendingPromiseResolution,
165  #[class(generic)]
166  #[error(
167    "Cannot evaluate dynamically imported module, because JavaScript execution has been terminated"
168  )]
169  EvaluateDynamicImportedModule,
170  #[class(inherit)]
171  #[error(transparent)]
172  Module(ModuleConcreteError),
173  #[class(inherit)]
174  #[error(transparent)]
175  Data(DataError),
176  #[class(inherit)]
177  #[error(transparent)]
178  CreateCodeCache(#[from] CreateCodeCacheError),
179  #[class(inherit)]
180  #[error(transparent)]
181  ExtensionSnapshotMismatch(ExtensionSnapshotMismatchError),
182  #[class(inherit)]
183  #[error(transparent)]
184  ExtensionLazyInitCountMismatch(ExtensionLazyInitCountMismatchError),
185  #[class(inherit)]
186  #[error(transparent)]
187  ExtensionLazyInitOrderMismatch(ExtensionLazyInitOrderMismatchError),
188}
189
190impl CoreError {
191  pub fn print_with_cause(&self) -> String {
192    use std::error::Error;
193    let mut err_message = self.to_string();
194
195    if let Some(source) = self.source() {
196      err_message.push_str(&format!(
197        "\n\nCaused by:\n    {}",
198        source.to_string().replace("\n", "\n    ")
199      ));
200    }
201
202    err_message
203  }
204
205  pub fn to_v8_error(&self, scope: &mut v8::PinScope) -> v8::Global<v8::Value> {
206    self.as_kind().to_v8_error(scope)
207  }
208}
209
210impl CoreErrorKind {
211  pub fn to_v8_error(&self, scope: &mut v8::PinScope) -> v8::Global<v8::Value> {
212    let err_string = self.get_message().to_string();
213    let mut error_chain = vec![];
214    let mut intermediary_error: Option<&dyn Error> = Some(&self);
215
216    while let Some(err) = intermediary_error {
217      if let Some(source) = err.source() {
218        let source_str = source.to_string();
219        if source_str != err_string {
220          error_chain.push(source_str);
221        }
222
223        intermediary_error = Some(source);
224      } else {
225        intermediary_error = None;
226      }
227    }
228
229    let message = if !error_chain.is_empty() {
230      format!(
231        "{}\n  Caused by:\n    {}",
232        err_string,
233        error_chain.join("\n    ")
234      )
235    } else {
236      err_string
237    };
238
239    let exception =
240      js_class_and_message_to_exception(scope, &self.get_class(), &message);
241    v8::Global::new(scope, exception)
242  }
243}
244
245impl From<v8::DataError> for CoreError {
246  fn from(err: v8::DataError) -> Self {
247    CoreErrorKind::Data(DataError(err)).into_box()
248  }
249}
250
251/// Single-pass V8 string → Rust string via `write_utf8_into` (uses ValueView
252/// internally). Replaces the two-pass `to_rust_string_lossy` which does a
253/// `utf8_length` pre-scan before writing.
254#[inline]
255fn v8_to_rust_string(s: &v8::String, scope: &mut v8::Isolate) -> String {
256  let mut buf = String::new();
257  s.write_utf8_into(scope, &mut buf);
258  buf
259}
260
261pub fn throw_js_error_class(
262  scope: &mut v8::PinScope,
263  error: &dyn JsErrorClass,
264) {
265  let exception = js_class_and_message_to_exception(
266    scope,
267    &error.get_class(),
268    &error.get_message(),
269  );
270  scope.throw_exception(exception);
271}
272
273fn js_class_and_message_to_exception<'s, 'i>(
274  scope: &mut v8::PinScope<'s, 'i>,
275  _class: &str,
276  message: &str,
277) -> v8::Local<'s, v8::Value> {
278  let message = v8::String::new(scope, message).unwrap();
279  /*
280  commented out since this was previously only handling type errors, but this
281  change is breaking CLI, so visiting on a later date
282
283  match class {
284    TYPE_ERROR => v8::Exception::type_error(scope, message),
285    RANGE_ERROR => v8::Exception::range_error(scope, message),
286    REFERENCE_ERROR => v8::Exception::reference_error(scope, message),
287    SYNTAX_ERROR => v8::Exception::syntax_error(scope, message),
288    _ => v8::Exception::error(scope, message),
289  }*/
290  v8::Exception::type_error(scope, message)
291}
292
293pub fn to_v8_error<'s, 'i>(
294  scope: &mut v8::PinScope<'s, 'i>,
295  error: &dyn JsErrorClass,
296) -> v8::Local<'s, v8::Value> {
297  v8::tc_scope!(let tc_scope, scope);
298
299  let cb = JsRealm::exception_state_from_scope(tc_scope)
300    .js_build_custom_error_cb
301    .borrow()
302    .clone()
303    .expect("Custom error builder must be set");
304  let cb = cb.open(tc_scope);
305  let this = v8::undefined(tc_scope).into();
306  let class = v8::String::new(tc_scope, &error.get_class()).unwrap();
307  let message = v8::String::new(tc_scope, &error.get_message()).unwrap();
308  let mut args = vec![class.into(), message.into()];
309
310  let additional_properties = error
311    .get_additional_properties()
312    .map(|(key, value)| {
313      let key = v8::String::new(tc_scope, &key).unwrap().into();
314      let value = match value {
315        PropertyValue::String(value) => {
316          v8::String::new(tc_scope, &value).unwrap().into()
317        }
318        PropertyValue::Number(value) => v8::Number::new(tc_scope, value).into(),
319      };
320
321      v8::Array::new_with_elements(tc_scope, &[key, value]).into()
322    })
323    .collect::<Vec<_>>();
324
325  if !additional_properties.is_empty() {
326    args.push(
327      v8::Array::new_with_elements(tc_scope, &additional_properties).into(),
328    );
329  }
330
331  let maybe_exception = cb.call(tc_scope, this, &args);
332
333  match maybe_exception {
334    Some(exception) => exception,
335    None => {
336      // The JS error builder callback failed. This can happen when the
337      // stack is exhausted (e.g. Maximum call stack size exceeded) and
338      // the callback itself throws. Fall back to a plain V8 error
339      // instead of panicking.
340      if tc_scope.has_caught() {
341        tc_scope.reset();
342      }
343      message.into()
344    }
345  }
346}
347
348/// Effectively throw an uncatchable error. This will terminate runtime
349/// execution before any more JS code can run, except in the REPL where it
350/// should just output the error to the console.
351pub fn dispatch_exception<'s, 'i>(
352  scope: &mut v8::PinScope<'s, 'i>,
353  exception: v8::Local<'s, v8::Value>,
354  promise: bool,
355) {
356  let state = JsRuntime::state_from(scope);
357  if let Some(true) = state.with_inspector(|inspector| {
358    inspector.exception_thrown(scope, exception, false);
359    inspector.is_dispatching_message()
360  }) {
361    // This indicates that the fn is being called from a REPL. Skip termination.
362    return;
363  }
364
365  JsRealm::exception_state_from_scope(scope)
366    .set_dispatched_exception(v8::Global::new(scope, exception), promise);
367  scope.terminate_execution();
368}
369
370#[inline(always)]
371pub(crate) fn call_site_evals_key<'s, 'i>(
372  scope: &mut v8::PinScope<'s, 'i>,
373) -> v8::Local<'s, v8::Private> {
374  let name = v8_static_strings::CALL_SITE_EVALS.v8_string(scope).unwrap();
375  v8::Private::for_api(scope, Some(name))
376}
377
378/// A `JsError` represents an exception coming from V8, with stack frames and
379/// line numbers. The deno_cli crate defines another `JsError` type, which wraps
380/// the one defined here, that adds source map support and colorful formatting.
381/// When updating this struct, also update errors_are_equal_without_cause() in
382/// fmt_error.rs.
383#[derive(Debug, PartialEq, Clone, serde::Deserialize, serde::Serialize)]
384#[serde(rename_all = "camelCase")]
385pub struct JsError {
386  pub name: Option<String>,
387  pub message: Option<String>,
388  pub stack: Option<String>,
389  pub cause: Option<Box<JsError>>,
390  pub exception_message: String,
391  pub frames: Vec<JsStackFrame>,
392  pub source_line: Option<String>,
393  pub source_line_frame_index: Option<usize>,
394  pub aggregated: Option<Vec<JsError>>,
395  pub additional_properties: Vec<(String, String)>,
396}
397
398impl JsErrorClass for JsError {
399  fn get_class(&self) -> Cow<'static, str> {
400    if let Some(name) = &self.name {
401      Cow::Owned(name.clone())
402    } else {
403      Cow::Borrowed(GENERIC_ERROR)
404    }
405  }
406
407  fn get_message(&self) -> Cow<'static, str> {
408    if let Some(message) = &self.message {
409      Cow::Owned(message.clone())
410    } else {
411      Cow::Borrowed("")
412    }
413  }
414
415  fn get_additional_properties(&self) -> deno_error::AdditionalProperties {
416    Box::new(
417      self
418        .additional_properties
419        // todo(dsherret): why does JsErrorClass not allow having references within this struct?
420        .clone()
421        .into_iter()
422        .map(|(k, v)| {
423          (
424            Cow::Owned(k.to_string()),
425            PropertyValue::String(Cow::Owned(v.to_string())),
426          )
427        }),
428    )
429  }
430
431  fn get_ref(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
432    self
433  }
434}
435
436#[derive(Debug, Eq, PartialEq, Clone, serde::Deserialize, serde::Serialize)]
437#[serde(rename_all = "camelCase")]
438pub struct JsStackFrame {
439  pub type_name: Option<String>,
440  pub function_name: Option<String>,
441  pub method_name: Option<String>,
442  pub file_name: Option<String>,
443  pub line_number: Option<i64>,
444  pub column_number: Option<i64>,
445  pub eval_origin: Option<String>,
446  // Warning! isToplevel has inconsistent snake<>camel case, "typo" originates in v8:
447  // https://source.chromium.org/search?q=isToplevel&sq=&ss=chromium%2Fchromium%2Fsrc:v8%2F
448  #[serde(rename = "isToplevel")]
449  pub is_top_level: Option<bool>,
450  pub is_eval: bool,
451  pub is_native: bool,
452  pub is_constructor: bool,
453  pub is_async: bool,
454  pub is_promise_all: bool,
455  pub is_wasm: bool,
456  pub promise_index: Option<i64>,
457}
458
459/// Applies source map to the given location
460fn apply_source_map<'a>(
461  source_mapper: &mut crate::source_map::SourceMapper,
462  file_name: Cow<'a, str>,
463  line_number: i64,
464  column_number: i64,
465) -> (Cow<'a, str>, i64, i64) {
466  match source_mapper.apply_source_map(
467    &file_name,
468    line_number as u32,
469    column_number as u32,
470  ) {
471    SourceMapApplication::Unchanged => (file_name, line_number, column_number),
472    SourceMapApplication::LineAndColumn {
473      line_number,
474      column_number,
475    } => (file_name, line_number.into(), column_number.into()),
476    SourceMapApplication::LineAndColumnAndFileName {
477      file_name,
478      line_number,
479      column_number,
480    } => (file_name.into(), line_number.into(), column_number.into()),
481  }
482}
483
484/// Parses an eval origin string from V8, returning
485/// the contents before the location,
486/// (the file name, line number, and column number), and
487/// the contents after the location.
488///
489/// # Example
490/// ```ignore
491/// assert_eq!(
492///   parse_eval_origin("eval at foo (bar at (file://a.ts:1:2))"),
493///   Some(("eval at foo (bar at (", ("file://a.ts", 1, 2), "))")),
494/// );
495/// ```
496///
497fn parse_eval_origin(
498  eval_origin: &str,
499) -> Option<(&str, (&str, i64, i64), &str)> {
500  // The eval origin string we get from V8 looks like
501  // `eval at ${function_name} (${origin})`
502  // where origin can be either a file name, like
503  // "eval at foo (file:///path/to/script.ts:1:2)"
504  // or a nested eval origin, like
505  // "eval at foo (eval at bar (file:///path/to/script.ts:1:2))"
506  //
507  let eval_at = "eval at ";
508  // only the innermost eval origin can have location info, so find the last
509  // "eval at", then continue parsing the rest of the string
510  let mut innermost_start = eval_origin.rfind(eval_at)? + eval_at.len();
511  // skip over the function name
512  innermost_start += eval_origin[innermost_start..].find('(')? + 1;
513  if innermost_start >= eval_origin.len() {
514    // malformed
515    return None;
516  }
517
518  // from the right, split by ":" to get the column number, line number, file name
519  // (in that order, since we're iterating from the right). e.g.
520  // eval at foo (eval at bar (file://foo.ts:1:2))
521  //                           ^^^^^^^^^^^^^ ^ ^^^
522  let mut parts = eval_origin[innermost_start..].rsplitn(3, ':');
523  // the part with the column number will include extra stuff, the actual number ends at
524  // the closing paren
525  let column_number_with_rest = parts.next()?;
526  let column_number_end = column_number_with_rest.find(')')?;
527  let column_number = column_number_with_rest[..column_number_end]
528    .parse::<i64>()
529    .ok()?;
530  let line_number = parts.next()?.parse::<i64>().ok()?;
531  let file_name = parts.next()?;
532  // The column number starts after the last occurring ":".
533  let column_start = eval_origin.rfind(':')? + 1;
534  // the innermost origin ends at the end of the column number
535  let innermost_end = column_start + column_number_end;
536  Some((
537    &eval_origin[..innermost_start],
538    (file_name, line_number, column_number),
539    &eval_origin[innermost_end..],
540  ))
541}
542
543impl JsStackFrame {
544  pub fn from_location(
545    file_name: Option<String>,
546    line_number: Option<i64>,
547    column_number: Option<i64>,
548  ) -> Self {
549    Self {
550      type_name: None,
551      function_name: None,
552      method_name: None,
553      file_name,
554      line_number,
555      column_number,
556      eval_origin: None,
557      is_top_level: None,
558      is_eval: false,
559      is_native: false,
560      is_constructor: false,
561      is_async: false,
562      is_promise_all: false,
563      is_wasm: false,
564      promise_index: None,
565    }
566  }
567
568  /// Creates a `JsStackFrame` from a `CallSite`` JS object,
569  /// provided by V8.
570  fn from_callsite_object<'s, 'i>(
571    scope: &mut v8::PinScope<'s, 'i>,
572    callsite: v8::Local<'s, v8::Object>,
573  ) -> Option<Self> {
574    macro_rules! call {
575      ($key: ident : $t: ty) => {{
576        let res = call_method(scope, callsite, $key, &[])?;
577        let res: $t = match serde_v8::from_v8(scope, res) {
578          Ok(res) => res,
579          Err(err) => {
580            let message = format!(
581              "Failed to deserialize return value from callsite property '{}' to correct type: {err:?}.",
582              $key
583            );
584            let message = v8::String::new(scope, &message).unwrap();
585            let exception = v8::Exception::type_error(scope, message);
586            scope.throw_exception(exception);
587            return None;
588          }
589        };
590        res
591      }};
592      ($key: ident) => { call!($key : _) };
593    }
594
595    let raw_file_name = call!(GET_FILE_NAME : Option<String>);
596    let is_wasm = raw_file_name
597      .as_deref()
598      .map(|f| f.starts_with("wasm://"))
599      .unwrap_or(false);
600
601    // For wasm frames, skip source map application — source maps don't apply to wasm.
602    // V8 returns the function index via getLineNumber() and byte offset via getColumnNumber().
603    let (file_name, line_number, column_number) = if is_wasm {
604      (
605        raw_file_name,
606        call!(GET_LINE_NUMBER),
607        call!(GET_COLUMN_NUMBER),
608      )
609    } else {
610      let state = JsRuntime::state_from(scope);
611      let mut source_mapper = state.source_mapper.borrow_mut();
612      // apply source map
613      match (
614        raw_file_name,
615        call!(GET_LINE_NUMBER),
616        call!(GET_COLUMN_NUMBER),
617      ) {
618        (Some(f), Some(l), Some(c)) => {
619          let (file_name, line_num, col_num) =
620            apply_source_map(&mut source_mapper, f.into(), l, c);
621          (Some(file_name.into_owned()), Some(line_num), Some(col_num))
622        }
623        (f, l, c) => (f, l, c),
624      }
625    };
626
627    // apply source map to the eval origin, if the error originates from `eval`ed code
628    let eval_origin = if is_wasm {
629      None
630    } else {
631      call!(GET_EVAL_ORIGIN: Option<String>).and_then(|o| {
632        let Some((before, (file, line, col), after)) = parse_eval_origin(&o)
633        else {
634          return Some(o);
635        };
636        let state = JsRuntime::state_from(scope);
637        let mut source_mapper = state.source_mapper.borrow_mut();
638        let (file, line, col) =
639          apply_source_map(&mut source_mapper, file.into(), line, col);
640        Some(format!("{before}{file}:{line}:{col}{after}"))
641      })
642    };
643
644    Some(Self {
645      file_name,
646      line_number,
647      column_number,
648      eval_origin,
649      type_name: call!(GET_TYPE_NAME),
650      function_name: call!(GET_FUNCTION_NAME),
651      method_name: call!(GET_METHOD_NAME),
652      is_top_level: call!(IS_TOPLEVEL),
653      is_eval: call!(IS_EVAL),
654      is_native: call!(IS_NATIVE),
655      is_constructor: call!(IS_CONSTRUCTOR),
656      is_async: call!(IS_ASYNC),
657      is_promise_all: call!(IS_PROMISE_ALL),
658      is_wasm,
659      promise_index: call!(GET_PROMISE_INDEX),
660    })
661  }
662
663  /// Gets the source mapped stack frame corresponding to the
664  /// (script_resource_name, line_number, column_number) from a v8 message.
665  /// For non-syntax errors, it should also correspond to the first stack frame.
666  pub fn from_v8_message<'s, 'i>(
667    scope: &mut v8::PinScope<'s, 'i>,
668    message: v8::Local<'s, v8::Message>,
669  ) -> Option<Self> {
670    let f = message.get_script_resource_name(scope)?;
671    let f: v8::Local<v8::String> = f.try_into().ok()?;
672    let f = v8_to_rust_string(&f, scope);
673    let l = message.get_line_number(scope)? as i64;
674    // V8's column numbers are 0-based, we want 1-based.
675    let c = message.get_start_column() as i64 + 1;
676    let state = JsRuntime::state_from(scope);
677    let mut source_mapper = state.source_mapper.borrow_mut();
678    let (file_name, line_num, col_num) =
679      apply_source_map(&mut source_mapper, f.into(), l, c);
680    Some(JsStackFrame::from_location(
681      Some(file_name.into_owned()),
682      Some(line_num),
683      Some(col_num),
684    ))
685  }
686
687  pub fn maybe_format_location(&self) -> Option<String> {
688    Some(format!(
689      "{}:{}:{}",
690      self.file_name.as_ref()?,
691      self.line_number?,
692      self.column_number?
693    ))
694  }
695}
696
697#[inline(always)]
698fn get_property<'s, 'i>(
699  scope: &mut v8::PinScope<'s, 'i>,
700  object: v8::Local<'s, v8::Object>,
701  key: FastStaticString,
702) -> Option<v8::Local<'s, v8::Value>> {
703  let key = key.v8_string(scope).unwrap();
704  object.get(scope, key.into())
705}
706
707fn call_method<'s, 'i, T>(
708  scope: &mut v8::PinScope<'s, 'i>,
709  object: v8::Local<'s, v8::Object>,
710  key: FastStaticString,
711  args: &[v8::Local<'s, v8::Value>],
712) -> Option<v8::Local<'s, T>>
713where
714  v8::Local<'s, T>: TryFrom<v8::Local<'s, v8::Value>, Error: Debug>,
715{
716  let func = match get_property(scope, object, key)?.try_cast::<v8::Function>()
717  {
718    Ok(func) => func,
719    Err(err) => {
720      let message =
721        format!("Callsite property '{key}' is not a function: {err}");
722      let message = v8::String::new(scope, &message).unwrap();
723      let exception = v8::Exception::type_error(scope, message);
724      scope.throw_exception(exception);
725      return None;
726    }
727  };
728
729  let res = func.call(scope, object.into(), args)?;
730
731  let result = match v8::Local::try_from(res) {
732    Ok(result) => result,
733    Err(err) => {
734      let message = format!(
735        "Failed to cast callsite method '{key}' return value to correct value: {err:?}."
736      );
737      let message = v8::String::new(scope, &message).unwrap();
738      let exception = v8::Exception::type_error(scope, message);
739      scope.throw_exception(exception);
740      return None;
741    }
742  };
743
744  Some(result)
745}
746
747#[derive(Debug, Default, serde::Deserialize)]
748pub(crate) struct NativeJsError {
749  pub name: Option<String>,
750  pub message: Option<String>,
751  // Warning! .stack is special so handled by itself
752  // stack: Option<String>,
753}
754
755impl JsError {
756  /// Compares all properties of JsError, except for JsError::cause. This function is used to
757  /// detect that 2 JsError objects in a JsError::cause chain are identical, ie. there is a recursive cause.
758  ///
759  /// We don't have access to object identity here, so we do it via field comparison. Ideally this should
760  /// be able to maintain object identity somehow.
761  pub fn is_same_error(&self, other: &JsError) -> bool {
762    let a = self;
763    let b = other;
764    // `a.cause == b.cause` omitted, because it is absent in recursive errors,
765    // despite the error being identical to a previously seen one.
766    a.name == b.name
767      && a.message == b.message
768      && a.stack == b.stack
769      // TODO(mmastrac): we need consistency around when we insert "in promise" and when we don't. For now, we
770      // are going to manually replace this part of the string.
771      && (a.exception_message == b.exception_message
772      || a.exception_message.replace(" (in promise) ", " ") == b.exception_message.replace(" (in promise) ", " "))
773      && a.frames == b.frames
774      && a.source_line == b.source_line
775      && a.source_line_frame_index == b.source_line_frame_index
776      && a.aggregated == b.aggregated
777  }
778
779  pub fn from_v8_exception<'s, 'i>(
780    scope: &mut v8::PinScope<'s, 'i>,
781    exception: v8::Local<'s, v8::Value>,
782  ) -> Box<Self> {
783    Box::new(Self::inner_from_v8_exception(
784      scope,
785      exception,
786      Default::default(),
787    ))
788  }
789
790  pub fn from_v8_message<'s, 'i>(
791    scope: &mut v8::PinScope<'s, 'i>,
792    msg: v8::Local<'s, v8::Message>,
793  ) -> Box<Self> {
794    // Create a new HandleScope because we're creating a lot of new local
795    // handles below.
796    v8::scope!(let scope, scope);
797
798    let exception_message = v8_to_rust_string(&msg.get(scope), scope);
799
800    // Convert them into Vec<JsStackFrame>
801    let mut frames: Vec<JsStackFrame> = vec![];
802    let mut source_line = None;
803    let mut source_line_frame_index = None;
804
805    if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) {
806      frames = vec![stack_frame];
807    }
808    {
809      let state = JsRuntime::state_from(scope);
810      let mut source_mapper = state.source_mapper.borrow_mut();
811      for (i, frame) in frames.iter().enumerate() {
812        if let (Some(file_name), Some(line_number)) =
813          (&frame.file_name, frame.line_number)
814          && !file_name.trim_start_matches('[').starts_with("ext:")
815          && !file_name.starts_with("node:")
816        {
817          source_line = source_mapper.get_source_line(file_name, line_number);
818          source_line_frame_index = Some(i);
819          break;
820        }
821      }
822    }
823
824    Box::new(Self {
825      name: None,
826      message: None,
827      exception_message,
828      cause: None,
829      source_line,
830      source_line_frame_index,
831      frames,
832      stack: None,
833      aggregated: None,
834      additional_properties: vec![],
835    })
836  }
837
838  fn inner_from_v8_exception<'s, 'i>(
839    scope: &mut v8::PinScope<'s, 'i>,
840    exception: v8::Local<'s, v8::Value>,
841    mut seen: HashSet<v8::Local<'s, v8::Object>>,
842  ) -> Self {
843    // Create a new HandleScope because we're creating a lot of new local
844    // handles below.
845    v8::scope!(let scope, scope);
846
847    let msg = v8::Exception::create_message(scope, exception);
848
849    let mut exception_message = None;
850    let exception_state = JsRealm::exception_state_from_scope(scope);
851
852    let js_format_exception_cb =
853      exception_state.js_format_exception_cb.borrow().clone();
854    if let Some(format_exception_cb) = js_format_exception_cb {
855      let format_exception_cb = format_exception_cb.open(scope);
856      let this = v8::undefined(scope).into();
857      let formatted = format_exception_cb.call(scope, this, &[exception]);
858      if let Some(formatted) = formatted
859        && let Ok(formatted) = formatted.try_cast::<v8::String>()
860      {
861        exception_message = Some(v8_to_rust_string(&formatted, scope));
862      }
863    }
864
865    if is_instance_of_error(scope, exception) {
866      let v8_exception = exception;
867      // The exception is a JS Error object.
868      let exception: v8::Local<v8::Object> = exception.try_into().unwrap();
869      let cause = get_property(scope, exception, v8_static_strings::CAUSE);
870      let e: NativeJsError =
871        serde_v8::from_v8(scope, exception.into()).unwrap_or_default();
872      // Get the message by formatting error.name and error.message.
873      let name = e.name.clone().unwrap_or_else(|| GENERIC_ERROR.to_string());
874      let message_prop = e.message.clone().unwrap_or_default();
875      let exception_message = exception_message.unwrap_or_else(|| {
876        if !name.is_empty() && !message_prop.is_empty() {
877          format!("Uncaught {name}: {message_prop}")
878        } else if !name.is_empty() {
879          format!("Uncaught {name}")
880        } else if !message_prop.is_empty() {
881          format!("Uncaught {message_prop}")
882        } else {
883          "Uncaught".to_string()
884        }
885      });
886      let cause = cause.and_then(|cause| {
887        if cause.is_undefined() || seen.contains(&exception) {
888          None
889        } else {
890          seen.insert(exception);
891          Some(Box::new(JsError::inner_from_v8_exception(
892            scope, cause, seen,
893          )))
894        }
895      });
896
897      // Access error.stack to ensure that prepareStackTrace() has been called.
898      // This should populate error.#callSiteEvals.
899      let stack = get_property(scope, exception, v8_static_strings::STACK);
900      let stack: Option<v8::Local<v8::String>> =
901        stack.and_then(|s| s.try_into().ok());
902      let stack = stack.map(|s| v8_to_rust_string(&s, scope));
903
904      // Read an array of structured frames from error.#callSiteEvals.
905      let frames_v8 = {
906        let key = call_site_evals_key(scope);
907        exception.get_private(scope, key)
908      };
909      // Ignore non-array values
910      let frames_v8: Option<v8::Local<v8::Array>> =
911        frames_v8.and_then(|a| a.try_into().ok());
912
913      // Convert them into Vec<JsStackFrame>
914      let mut frames: Vec<JsStackFrame> = match frames_v8 {
915        Some(frames_v8) => {
916          let mut buf = Vec::with_capacity(frames_v8.length() as usize);
917          for i in 0..frames_v8.length() {
918            let callsite = frames_v8.get_index(scope, i).unwrap().cast();
919            v8::tc_scope!(let tc_scope, scope);
920
921            let Some(stack_frame) =
922              JsStackFrame::from_callsite_object(tc_scope, callsite)
923            else {
924              let exc = tc_scope.exception().expect(
925                "JsStackFrame::from_callsite_object raised an exception",
926              );
927              let message = match exc.to_string(tc_scope) {
928                Some(s) => v8_to_rust_string(&s, tc_scope),
929                None => String::new(),
930              };
931              #[allow(
932                clippy::print_stderr,
933                reason = "intentional warning output"
934              )]
935              {
936                eprintln!(
937                  "warning: Failed to create JsStackFrame from callsite object: {message}. This is a bug in deno"
938                );
939              }
940              break;
941            };
942            buf.push(stack_frame);
943          }
944          buf
945        }
946        None => vec![],
947      };
948      let mut source_line = None;
949      let mut source_line_frame_index = None;
950
951      // When the stack frame array is empty, but the source location given by
952      // (script_resource_name, line_number, start_column + 1) exists, this is
953      // likely a syntax error. For the sake of formatting we treat it like it
954      // was given as a single stack frame.
955      if frames.is_empty()
956        && let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg)
957      {
958        frames = vec![stack_frame];
959      }
960      {
961        let state = JsRuntime::state_from(scope);
962        let mut source_mapper = state.source_mapper.borrow_mut();
963        for (i, frame) in frames.iter().enumerate() {
964          if let (Some(file_name), Some(line_number)) =
965            (&frame.file_name, frame.line_number)
966            && !file_name.trim_start_matches('[').starts_with("ext:")
967            && !file_name.starts_with("node:")
968          {
969            source_line = source_mapper.get_source_line(file_name, line_number);
970            source_line_frame_index = Some(i);
971            break;
972          }
973        }
974      }
975
976      let mut aggregated: Option<Vec<JsError>> = None;
977      if is_aggregate_error(scope, v8_exception) {
978        // Read an array of stored errors, this is only defined for `AggregateError`
979        let aggregated_errors =
980          get_property(scope, exception, v8_static_strings::ERRORS);
981        let aggregated_errors: Option<v8::Local<v8::Array>> =
982          aggregated_errors.and_then(|a| a.try_into().ok());
983
984        if let Some(errors) = aggregated_errors
985          && errors.length() > 0
986        {
987          let mut agg = vec![];
988          for i in 0..errors.length() {
989            let error = errors.get_index(scope, i).unwrap();
990            let js_error = Self::from_v8_exception(scope, error);
991            agg.push(*js_error);
992          }
993          aggregated = Some(agg);
994        }
995      };
996
997      let additional_properties_string =
998        v8::String::new(scope, "errorAdditionalPropertyKeys").unwrap();
999      let additional_properties_key =
1000        v8::Symbol::for_key(scope, additional_properties_string);
1001      let additional_properties =
1002        exception.get(scope, additional_properties_key.into());
1003
1004      let additional_properties = if let Some(arr) =
1005        additional_properties.and_then(|keys| keys.try_cast::<v8::Array>().ok())
1006      {
1007        let mut out = Vec::with_capacity(arr.length() as usize);
1008
1009        for i in 0..arr.length() {
1010          let Some(key) = arr.get_index(scope, i) else {
1011            continue;
1012          };
1013          let key_name = match key.to_string(scope) {
1014            Some(s) => v8_to_rust_string(&s, scope),
1015            None => continue,
1016          };
1017
1018          let Some(val) = exception.get(scope, key) else {
1019            continue;
1020          };
1021          let val_str = match val.to_string(scope) {
1022            Some(s) => v8_to_rust_string(&s, scope),
1023            None => continue,
1024          };
1025          out.push((key_name, val_str));
1026        }
1027
1028        out
1029      } else {
1030        vec![]
1031      };
1032
1033      Self {
1034        name: e.name,
1035        message: e.message,
1036        exception_message,
1037        cause,
1038        source_line,
1039        source_line_frame_index,
1040        frames,
1041        stack,
1042        aggregated,
1043        additional_properties,
1044      }
1045    } else {
1046      let exception_message = exception_message
1047        .unwrap_or_else(|| v8_to_rust_string(&msg.get(scope), scope));
1048      // The exception is not a JS Error object.
1049      // Get the message given by V8::Exception::create_message(), and provide
1050      // empty frames.
1051      Self {
1052        name: None,
1053        message: None,
1054        exception_message,
1055        cause: None,
1056        source_line: None,
1057        source_line_frame_index: None,
1058        frames: vec![],
1059        stack: None,
1060        aggregated: None,
1061        additional_properties: vec![],
1062      }
1063    }
1064  }
1065}
1066
1067impl std::error::Error for JsError {}
1068
1069impl Display for JsError {
1070  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1071    if let Some(stack) = &self.stack {
1072      let stack_lines = stack.lines();
1073      if stack_lines.count() > 1 {
1074        return write!(f, "{stack}");
1075      }
1076    }
1077    write!(f, "{}", self.exception_message)?;
1078    let location = self.frames.first().and_then(|f| f.maybe_format_location());
1079    if let Some(location) = location {
1080      write!(f, "\n    at {location}")?;
1081    }
1082    Ok(())
1083  }
1084}
1085
1086/// Implements `value instanceof primordials.Error` in JS. Similar to
1087/// `Value::is_native_error()` but more closely matches the semantics
1088/// of `instanceof`. `Value::is_native_error()` also checks for static class
1089/// inheritance rather than just scanning the prototype chain, which doesn't
1090/// work with our WebIDL implementation of `DOMException`.
1091pub(crate) fn is_instance_of_error<'s, 'i>(
1092  scope: &mut v8::PinScope<'s, 'i>,
1093  value: v8::Local<'s, v8::Value>,
1094) -> bool {
1095  if !value.is_object() {
1096    return false;
1097  }
1098  let message = v8::String::empty(scope);
1099  let error_prototype = v8::Exception::error(scope, message)
1100    .to_object(scope)
1101    .unwrap()
1102    .get_prototype(scope)
1103    .unwrap();
1104  let mut maybe_prototype =
1105    value.to_object(scope).unwrap().get_prototype(scope);
1106  while let Some(prototype) = maybe_prototype {
1107    if !prototype.is_object() {
1108      return false;
1109    }
1110    if prototype.strict_equals(error_prototype) {
1111      return true;
1112    }
1113    maybe_prototype = prototype
1114      .to_object(scope)
1115      .and_then(|o| o.get_prototype(scope));
1116  }
1117  false
1118}
1119
1120/// Implements `value instanceof primordials.AggregateError` in JS,
1121/// by walking the prototype chain, and comparing each links constructor `name` property.
1122///
1123/// NOTE: There is currently no way to detect `AggregateError` via `rusty_v8`,
1124/// as v8 itself doesn't expose `v8__Exception__AggregateError`,
1125/// and we cannot create bindings for it. This forces us to rely on `name` inference.
1126pub(crate) fn is_aggregate_error<'s, 'i>(
1127  scope: &mut v8::PinScope<'s, 'i>,
1128  value: v8::Local<'s, v8::Value>,
1129) -> bool {
1130  let mut maybe_prototype = Some(value);
1131  while let Some(prototype) = maybe_prototype {
1132    if !prototype.is_object() {
1133      return false;
1134    }
1135
1136    let prototype = prototype.to_object(scope).unwrap();
1137    let prototype_name =
1138      match get_property(scope, prototype, v8_static_strings::CONSTRUCTOR) {
1139        Some(constructor) => {
1140          let ctor = constructor.to_object(scope).unwrap();
1141          get_property(scope, ctor, v8_static_strings::NAME)
1142            .and_then(|v| v.to_string(scope))
1143            .map(|s| v8_to_rust_string(&s, scope))
1144        }
1145        None => return false,
1146      };
1147
1148    if prototype_name == Some(String::from("AggregateError")) {
1149      return true;
1150    }
1151
1152    maybe_prototype = prototype.get_prototype(scope);
1153  }
1154
1155  false
1156}
1157
1158/// Check if the error has a proper stack trace. The stack trace checked is the
1159/// one passed to `prepareStackTrace()`, not `msg.get_stack_trace()`.
1160pub(crate) fn has_call_site<'s, 'i>(
1161  scope: &mut v8::PinScope<'s, 'i>,
1162  exception: v8::Local<'s, v8::Value>,
1163) -> bool {
1164  if !exception.is_object() {
1165    return false;
1166  }
1167  let exception = exception.to_object(scope).unwrap();
1168  // Access error.stack to ensure that prepareStackTrace() has been called.
1169  // This should populate error.#callSiteEvals.
1170  get_property(scope, exception, v8_static_strings::STACK);
1171  let frames_v8 = {
1172    let key = call_site_evals_key(scope);
1173    exception.get_private(scope, key)
1174  };
1175  let frames_v8: Option<v8::Local<v8::Array>> =
1176    frames_v8.and_then(|a| a.try_into().ok());
1177  if let Some(frames_v8) = frames_v8
1178    && frames_v8.length() > 0
1179  {
1180    return true;
1181  }
1182  false
1183}
1184
1185const DATA_URL_ABBREV_THRESHOLD: usize = 150;
1186
1187fn to_percent_decoded_str(s: &str) -> String {
1188  match percent_encoding::percent_decode_str(s).decode_utf8() {
1189    Ok(s) => s.to_string(),
1190    // when failed to decode, return the original string
1191    Err(_) => s.to_string(),
1192  }
1193}
1194
1195pub fn relative_specifier(from: &Url, to: &Url) -> Option<String> {
1196  let is_dir = to.path().ends_with('/');
1197
1198  if is_dir && from == to {
1199    return Some("./".to_string());
1200  }
1201
1202  let text = from.make_relative(to)?;
1203  let text = if text.starts_with("../") || text.starts_with("./") {
1204    text
1205  } else {
1206    format!("./{text}")
1207  };
1208  Some(to_percent_decoded_str(&text))
1209}
1210
1211/// Like `relative_specifier`, but returns None if the path would require going up directories.
1212fn relative_specifier_within(from: &Url, to: &Url) -> Option<String> {
1213  let relative = relative_specifier(from, to)?;
1214  if relative.starts_with("../") {
1215    return None;
1216  }
1217  Some(relative)
1218}
1219
1220pub struct FileNameParts<'a> {
1221  pub working_dir_path: Option<Cow<'a, str>>,
1222  pub file_name: Cow<'a, str>,
1223}
1224
1225impl<'a> FileNameParts<'a> {
1226  pub fn into_owned(self) -> FileNameParts<'static> {
1227    FileNameParts {
1228      working_dir_path: self.working_dir_path.map(|s| match s {
1229        Cow::Borrowed(s) => Cow::Owned(s.to_owned()),
1230        Cow::Owned(s) => Cow::Owned(s),
1231      }),
1232      file_name: match self.file_name {
1233        Cow::Borrowed(s) => Cow::Owned(s.to_owned()),
1234        Cow::Owned(s) => Cow::Owned(s),
1235      },
1236    }
1237  }
1238}
1239
1240pub fn format_file_name<'a>(
1241  file_name: &'a str,
1242  maybe_initial_cwd: Option<&Url>,
1243) -> FileNameParts<'a> {
1244  abbrev_file_name(file_name, maybe_initial_cwd).unwrap_or_else(|| {
1245    // same as to_percent_decoded_str() in cli/util/path.rs
1246    match percent_encoding::percent_decode_str(file_name).decode_utf8() {
1247      Ok(file_name) => FileNameParts {
1248        working_dir_path: None,
1249        file_name,
1250      },
1251      // when failing as utf-8, just return the original string
1252      Err(_) => FileNameParts {
1253        working_dir_path: None,
1254        file_name: file_name.into(),
1255      },
1256    }
1257  })
1258}
1259
1260fn abbrev_file_name<'a>(
1261  file_name: &'a str,
1262  maybe_initial_cwd: Option<&Url>,
1263) -> Option<FileNameParts<'a>> {
1264  let Ok(url) = Url::parse(file_name) else {
1265    return None;
1266  };
1267
1268  // Handle data URLs - abbreviate if too long
1269  if url.scheme() == "data" {
1270    if file_name.len() <= DATA_URL_ABBREV_THRESHOLD {
1271      return Some(FileNameParts {
1272        working_dir_path: None,
1273        file_name: file_name.into(),
1274      });
1275    }
1276    let (head, tail) = url.path().split_once(',')?;
1277    let len = tail.len();
1278    let start = tail.get(0..20)?;
1279    let end = tail.get(len - 20..)?;
1280    return Some(FileNameParts {
1281      working_dir_path: None,
1282      file_name: format!("{}:{},{}......{}", url.scheme(), head, start, end)
1283        .into(),
1284    });
1285  }
1286
1287  // For file:// URLs, use relative paths only if the file is within the cwd
1288  if let Some(initial_cwd) = maybe_initial_cwd
1289    && let Some(relative) = relative_specifier_within(initial_cwd, &url)
1290  {
1291    return Some(FileNameParts {
1292      working_dir_path: Some(initial_cwd.to_string().into()),
1293      file_name: relative.into(),
1294    });
1295  }
1296
1297  // For all other cases, use absolute paths
1298  None
1299}
1300
1301fn format_eval_origin<'a>(
1302  eval_origin: &'a str,
1303  maybe_initial_cwd: Option<&Url>,
1304) -> Cow<'a, str> {
1305  let Some((before, (file, line, col), after)) = parse_eval_origin(eval_origin)
1306  else {
1307    return eval_origin.into();
1308  };
1309  let formatted_file = format_file_name(file, maybe_initial_cwd);
1310
1311  Cow::Owned(format!(
1312    "{before}{}:{line}:{col}{after}",
1313    if let Some(working_dir_path) = formatted_file.working_dir_path {
1314      Cow::Owned(format!(
1315        "{}{}",
1316        working_dir_path,
1317        formatted_file.file_name.trim_start_matches("./")
1318      ))
1319    } else {
1320      formatted_file.file_name
1321    }
1322  ))
1323}
1324
1325pub(crate) fn exception_to_err_result<'s, 'i, T>(
1326  scope: &mut v8::PinScope<'s, 'i>,
1327  exception: v8::Local<'s, v8::Value>,
1328  in_promise: bool,
1329  clear_error: bool,
1330) -> Result<T, Box<JsError>> {
1331  Err(exception_to_err(scope, exception, in_promise, clear_error))
1332}
1333
1334pub fn exception_to_err<'s, 'i>(
1335  scope: &mut v8::PinScope<'s, 'i>,
1336  exception: v8::Local<'s, v8::Value>,
1337  mut in_promise: bool,
1338  clear_error: bool,
1339) -> Box<JsError> {
1340  let state = JsRealm::exception_state_from_scope(scope);
1341
1342  let mut was_terminating_execution = scope.is_execution_terminating();
1343
1344  // If TerminateExecution was called, cancel isolate termination so that the
1345  // exception can be created. Note that `scope.is_execution_terminating()` may
1346  // have returned false if TerminateExecution was indeed called but there was
1347  // no JS to execute after the call.
1348  scope.cancel_terminate_execution();
1349  let exception = match state.get_dispatched_exception_as_local(scope) {
1350    Some(dispatched_exception) => {
1351      // If termination is the result of a `reportUnhandledException` call, we want
1352      // to use the exception that was passed to it rather than the exception that
1353      // was passed to this function.
1354      in_promise = state.is_dispatched_exception_promise();
1355      if clear_error {
1356        state.clear_error();
1357        was_terminating_execution = false;
1358      }
1359      dispatched_exception
1360    }
1361    _ => {
1362      if was_terminating_execution && exception.is_null_or_undefined() {
1363        // If we are terminating and there is no exception, throw `new Error("execution terminated")``.
1364        let message = v8::String::new(scope, "execution terminated").unwrap();
1365        v8::Exception::error(scope, message)
1366      } else {
1367        // Otherwise re-use the exception
1368        exception
1369      }
1370    }
1371  };
1372
1373  let mut js_error = JsError::from_v8_exception(scope, exception);
1374  if in_promise {
1375    js_error.exception_message = format!(
1376      "Uncaught (in promise) {}",
1377      js_error.exception_message.trim_start_matches("Uncaught ")
1378    );
1379  }
1380
1381  if was_terminating_execution {
1382    // Resume exception termination.
1383    scope.terminate_execution();
1384  }
1385
1386  js_error
1387}
1388
1389v8_static_strings::v8_static_strings! {
1390  ERROR = "Error",
1391  GET_FILE_NAME = "getFileName",
1392  GET_SCRIPT_NAME_OR_SOURCE_URL = "getScriptNameOrSourceURL",
1393  GET_THIS = "getThis",
1394  GET_TYPE_NAME = "getTypeName",
1395  GET_FUNCTION = "getFunction",
1396  GET_FUNCTION_NAME = "getFunctionName",
1397  GET_METHOD_NAME = "getMethodName",
1398  GET_LINE_NUMBER = "getLineNumber",
1399  GET_COLUMN_NUMBER = "getColumnNumber",
1400  GET_EVAL_ORIGIN = "getEvalOrigin",
1401  IS_TOPLEVEL = "isToplevel",
1402  IS_EVAL = "isEval",
1403  IS_NATIVE = "isNative",
1404  IS_CONSTRUCTOR = "isConstructor",
1405  IS_ASYNC = "isAsync",
1406  IS_PROMISE_ALL = "isPromiseAll",
1407  GET_PROMISE_INDEX = "getPromiseIndex",
1408  TO_STRING = "toString",
1409  PREPARE_STACK_TRACE = "prepareStackTrace",
1410  ORIGINAL = "deno_core::original_call_site",
1411  SOURCE_MAPPED_INFO = "deno_core::source_mapped_call_site_info",
1412  ERROR_RECEIVER_IS_NOT_VALID_CALLSITE_OBJECT = "The receiver is not a valid callsite object.",
1413}
1414
1415#[inline(always)]
1416pub(crate) fn original_call_site_key<'s, 'i>(
1417  scope: &mut v8::PinScope<'s, 'i>,
1418) -> v8::Local<'s, v8::Private> {
1419  let name = ORIGINAL.v8_string(scope).unwrap();
1420  v8::Private::for_api(scope, Some(name))
1421}
1422
1423pub(crate) fn source_mapped_info_key<'s, 'i>(
1424  scope: &mut v8::PinScope<'s, 'i>,
1425) -> v8::Local<'s, v8::Private> {
1426  let name = SOURCE_MAPPED_INFO.v8_string(scope).unwrap();
1427  v8::Private::for_api(scope, Some(name))
1428}
1429
1430fn make_patched_callsite<'s, 'i>(
1431  scope: &mut v8::PinScope<'s, 'i>,
1432  callsite: v8::Local<'s, v8::Object>,
1433  prototype: v8::Local<'s, v8::Object>,
1434) -> v8::Local<'s, v8::Object> {
1435  let out_obj = v8::Object::with_prototype_and_properties(
1436    scope,
1437    prototype.into(),
1438    &[],
1439    &[],
1440  );
1441  let orig_key = original_call_site_key(scope);
1442  out_obj.set_private(scope, orig_key, callsite.into());
1443  out_obj
1444}
1445
1446fn original_call_site<'s, 'i>(
1447  scope: &mut v8::PinScope<'s, 'i>,
1448  this: v8::Local<'s, v8::Object>,
1449) -> Option<v8::Local<'s, v8::Object>> {
1450  let orig_key = original_call_site_key(scope);
1451  let Some(orig) = this
1452    .get_private(scope, orig_key)
1453    .and_then(|v| v8::Local::<v8::Object>::try_from(v).ok())
1454  else {
1455    let message = ERROR_RECEIVER_IS_NOT_VALID_CALLSITE_OBJECT
1456      .v8_string(scope)
1457      .unwrap();
1458    let exception = v8::Exception::type_error(scope, message);
1459    scope.throw_exception(exception);
1460    return None;
1461  };
1462  Some(orig)
1463}
1464
1465macro_rules! make_callsite_fn {
1466  ($fn:ident, $field:ident) => {
1467    pub fn $fn<'s, 'i>(
1468      scope: &mut v8::PinScope<'s, 'i>,
1469      args: v8::FunctionCallbackArguments<'s>,
1470      mut rv: v8::ReturnValue<'_>,
1471    ) {
1472      let Some(orig) = original_call_site(scope, args.this()) else {
1473        return;
1474      };
1475      let key = $field.v8_string(scope).unwrap().into();
1476      let orig_ret = orig
1477        .cast::<v8::Object>()
1478        .get(scope, key)
1479        .unwrap()
1480        .cast::<v8::Function>()
1481        .call(scope, orig.into(), &[]);
1482      rv.set(orig_ret.unwrap_or_else(|| v8::undefined(scope).into()));
1483    }
1484  };
1485}
1486
1487fn maybe_to_path_str(string: &str) -> Option<String> {
1488  if string.starts_with("file://") {
1489    Some(
1490      deno_path_util::url_to_file_path(&Url::parse(string).ok()?)
1491        .ok()?
1492        .to_string_lossy()
1493        .into_owned(),
1494    )
1495  } else {
1496    None
1497  }
1498}
1499
1500pub mod callsite_fns {
1501  use capacity_builder::StringBuilder;
1502
1503  use super::*;
1504  use crate::FromV8;
1505  use crate::ToV8;
1506  use crate::convert;
1507
1508  enum SourceMappedCallsiteInfo<'a> {
1509    Ref(v8::Local<'a, v8::Array>),
1510    Value {
1511      file_name: v8::Local<'a, v8::Value>,
1512      line_number: v8::Local<'a, v8::Value>,
1513      column_number: v8::Local<'a, v8::Value>,
1514    },
1515  }
1516  impl<'s> SourceMappedCallsiteInfo<'s> {
1517    #[inline]
1518    fn file_name<'i>(
1519      &self,
1520      scope: &mut v8::PinScope<'s, 'i>,
1521    ) -> v8::Local<'s, v8::Value> {
1522      match self {
1523        Self::Ref(array) => array.get_index(scope, 0).unwrap(),
1524        Self::Value { file_name, .. } => *file_name,
1525      }
1526    }
1527    #[inline]
1528    fn line_number<'i>(
1529      &self,
1530      scope: &mut v8::PinScope<'s, 'i>,
1531    ) -> v8::Local<'s, v8::Value> {
1532      match self {
1533        Self::Ref(array) => array.get_index(scope, 1).unwrap(),
1534        Self::Value { line_number, .. } => *line_number,
1535      }
1536    }
1537    #[inline]
1538    fn column_number<'i>(
1539      &self,
1540      scope: &mut v8::PinScope<'s, 'i>,
1541    ) -> v8::Local<'s, v8::Value> {
1542      match self {
1543        Self::Ref(array) => array.get_index(scope, 2).unwrap(),
1544        Self::Value { column_number, .. } => *column_number,
1545      }
1546    }
1547  }
1548
1549  type MaybeValue<'a> = Option<v8::Local<'a, v8::Value>>;
1550
1551  fn maybe_apply_source_map<'s, 'i>(
1552    scope: &mut v8::PinScope<'s, 'i>,
1553    file_name: MaybeValue<'s>,
1554    line_number: MaybeValue<'s>,
1555    column_number: MaybeValue<'s>,
1556  ) -> Option<(String, i64, i64)> {
1557    let file_name = serde_v8::to_utf8(file_name?.try_cast().ok()?, scope);
1558    let convert::Number(line_number) =
1559      FromV8::from_v8(scope, line_number?).ok()?;
1560    let convert::Number(column_number) =
1561      FromV8::from_v8(scope, column_number?).ok()?;
1562
1563    let state = JsRuntime::state_from(scope);
1564    let mut source_mapper = state.source_mapper.borrow_mut();
1565    let (mapped_file_name, mapped_line_number, mapped_column_number) =
1566      apply_source_map(
1567        &mut source_mapper,
1568        Cow::Owned(file_name),
1569        line_number,
1570        column_number,
1571      );
1572    Some((
1573      mapped_file_name.into_owned(),
1574      mapped_line_number,
1575      mapped_column_number,
1576    ))
1577  }
1578  fn source_mapped_call_site_info<'s, 'i>(
1579    scope: &mut v8::PinScope<'s, 'i>,
1580    callsite: v8::Local<'s, v8::Object>,
1581  ) -> Option<SourceMappedCallsiteInfo<'s>> {
1582    let key = source_mapped_info_key(scope);
1583    // return the cached value if it exists
1584    if let Some(info) = callsite.get_private(scope, key)
1585      && let Ok(array) = info.try_cast::<v8::Array>()
1586    {
1587      return Some(SourceMappedCallsiteInfo::Ref(array));
1588    }
1589    let orig_callsite = original_call_site(scope, callsite)?;
1590
1591    let file_name =
1592      call_method::<v8::Value>(scope, orig_callsite, super::GET_FILE_NAME, &[]);
1593    let line_number = call_method::<v8::Value>(
1594      scope,
1595      orig_callsite,
1596      super::GET_LINE_NUMBER,
1597      &[],
1598    );
1599    let column_number = call_method::<v8::Value>(
1600      scope,
1601      orig_callsite,
1602      super::GET_COLUMN_NUMBER,
1603      &[],
1604    );
1605
1606    // For wasm frames, skip source map application and cache the original values
1607    let is_wasm = file_name
1608      .and_then(|v| v.try_cast::<v8::String>().ok())
1609      .map(|s| serde_v8::to_utf8(s, scope).starts_with("wasm://"))
1610      .unwrap_or(false);
1611
1612    let info = v8::Array::new(scope, 3);
1613
1614    // if the types are right, apply the source map (unless wasm), otherwise just take them as is
1615    if !is_wasm
1616      && let Some((mapped_file_name, mapped_line_number, mapped_column_number)) =
1617        maybe_apply_source_map(scope, file_name, line_number, column_number)
1618    {
1619      let mapped_file_name_trimmed =
1620        maybe_to_path_str(&mapped_file_name).unwrap_or(mapped_file_name);
1621      let mapped_file_name = crate::FastString::from(mapped_file_name_trimmed)
1622        .v8_string(scope)
1623        .unwrap();
1624      let Ok(mapped_line_number) =
1625        convert::Number(mapped_line_number).to_v8(scope);
1626      let Ok(mapped_column_number) =
1627        convert::Number(mapped_column_number).to_v8(scope);
1628      info.set_index(scope, 0, mapped_file_name.into());
1629      info.set_index(scope, 1, mapped_line_number);
1630      info.set_index(scope, 2, mapped_column_number);
1631      callsite.set_private(scope, key, info.into());
1632      Some(SourceMappedCallsiteInfo::Value {
1633        file_name: mapped_file_name.into(),
1634        line_number: mapped_line_number,
1635        column_number: mapped_column_number,
1636      })
1637    } else {
1638      let file_name = file_name.unwrap_or_else(|| v8::undefined(scope).into());
1639      let line_number =
1640        line_number.unwrap_or_else(|| v8::undefined(scope).into());
1641      let column_number =
1642        column_number.unwrap_or_else(|| v8::undefined(scope).into());
1643      info.set_index(scope, 0, file_name);
1644      info.set_index(scope, 1, line_number);
1645      info.set_index(scope, 2, column_number);
1646      callsite.set_private(scope, key, info.into());
1647      Some(SourceMappedCallsiteInfo::Ref(info))
1648    }
1649  }
1650
1651  make_callsite_fn!(get_this, GET_THIS);
1652  make_callsite_fn!(get_type_name, GET_TYPE_NAME);
1653  make_callsite_fn!(get_function, GET_FUNCTION);
1654  make_callsite_fn!(get_function_name, GET_FUNCTION_NAME);
1655  make_callsite_fn!(get_method_name, GET_METHOD_NAME);
1656
1657  pub fn get_file_name<'s, 'i>(
1658    scope: &mut v8::PinScope<'s, 'i>,
1659    args: v8::FunctionCallbackArguments<'s>,
1660    mut rv: v8::ReturnValue<'_>,
1661  ) {
1662    if let Some(info) = source_mapped_call_site_info(scope, args.this()) {
1663      rv.set(info.file_name(scope));
1664    }
1665  }
1666
1667  pub fn get_line_number<'s, 'i>(
1668    scope: &mut v8::PinScope<'s, 'i>,
1669    args: v8::FunctionCallbackArguments<'s>,
1670    mut rv: v8::ReturnValue<'_>,
1671  ) {
1672    if let Some(info) = source_mapped_call_site_info(scope, args.this()) {
1673      rv.set(info.line_number(scope));
1674    }
1675  }
1676
1677  pub fn get_column_number<'s, 'i>(
1678    scope: &mut v8::PinScope<'s, 'i>,
1679    args: v8::FunctionCallbackArguments<'s>,
1680    mut rv: v8::ReturnValue<'_>,
1681  ) {
1682    if let Some(info) = source_mapped_call_site_info(scope, args.this()) {
1683      rv.set(info.column_number(scope));
1684    }
1685  }
1686
1687  make_callsite_fn!(get_eval_origin, GET_EVAL_ORIGIN);
1688  make_callsite_fn!(is_toplevel, IS_TOPLEVEL);
1689  make_callsite_fn!(is_eval, IS_EVAL);
1690  make_callsite_fn!(is_native, IS_NATIVE);
1691  make_callsite_fn!(is_constructor, IS_CONSTRUCTOR);
1692  make_callsite_fn!(is_async, IS_ASYNC);
1693  make_callsite_fn!(is_promise_all, IS_PROMISE_ALL);
1694  make_callsite_fn!(get_promise_index, GET_PROMISE_INDEX);
1695  make_callsite_fn!(
1696    get_script_name_or_source_url,
1697    GET_SCRIPT_NAME_OR_SOURCE_URL
1698  );
1699
1700  // the bulk of the to_string logic
1701  fn to_string_inner<'s, 'i>(
1702    scope: &mut v8::PinScope<'s, 'i>,
1703    this: v8::Local<'s, v8::Object>,
1704    orig: v8::Local<'s, v8::Object>,
1705    orig_to_string_v8: v8::Local<'s, v8::String>,
1706  ) -> Option<v8::Local<'s, v8::String>> {
1707    let orig_to_string = serde_v8::to_utf8(orig_to_string_v8, scope);
1708    // `this[kOriginalCallsite].getFileName()`
1709    let orig_file_name =
1710      call_method::<v8::Value>(scope, orig, GET_FILE_NAME, &[])
1711        .and_then(|v| v.try_cast::<v8::String>().ok())?;
1712    let orig_file_name = serde_v8::to_utf8(orig_file_name, scope);
1713
1714    // For wasm frames, V8's original toString() already formats correctly
1715    // (with wasm-function[N]:0xOFFSET), so return it unchanged.
1716    if orig_file_name.starts_with("wasm://") {
1717      return Some(orig_to_string_v8);
1718    }
1719
1720    let orig_line_number =
1721      call_method::<v8::Value>(scope, orig, GET_LINE_NUMBER, &[])
1722        .and_then(|v| v.try_cast::<v8::Number>().ok())?;
1723    let orig_column_number =
1724      call_method::<v8::Value>(scope, orig, GET_COLUMN_NUMBER, &[])
1725        .and_then(|v| v.try_cast::<v8::Number>().ok())?;
1726    let orig_line_number = orig_line_number.value() as i64;
1727    let orig_column_number = orig_column_number.value() as i64;
1728    let orig_file_name_line_col =
1729      fmt_file_line_col(&orig_file_name, orig_line_number, orig_column_number);
1730    let mapped = source_mapped_call_site_info(scope, this)?;
1731    let mapped_file_name = match mapped.file_name(scope).to_string(scope) {
1732      Some(s) => v8_to_rust_string(&s, scope),
1733      None => String::new(),
1734    };
1735    let mapped_line_num = mapped
1736      .line_number(scope)
1737      .try_cast::<v8::Number>()
1738      .ok()
1739      .map(|n| n.value() as i64)?;
1740    let mapped_col_num =
1741      mapped.column_number(scope).cast::<v8::Number>().value() as i64;
1742    let file_name_line_col =
1743      fmt_file_line_col(&mapped_file_name, mapped_line_num, mapped_col_num);
1744    // replace file URL with file path, and source map in original `toString`
1745    let to_string = orig_to_string
1746      .replace(&orig_file_name_line_col, &file_name_line_col)
1747      .replace(&orig_file_name, &mapped_file_name); // maybe unnecessary?
1748    Some(crate::FastString::from(to_string).v8_string(scope).unwrap())
1749  }
1750
1751  fn fmt_file_line_col(file: &str, line: i64, col: i64) -> String {
1752    StringBuilder::build(|builder| {
1753      builder.append(file);
1754      builder.append(':');
1755      builder.append(line);
1756      builder.append(':');
1757      builder.append(col);
1758    })
1759    .unwrap()
1760  }
1761
1762  pub fn to_string<'s, 'i>(
1763    scope: &mut v8::PinScope<'s, 'i>,
1764    args: v8::FunctionCallbackArguments<'s>,
1765    mut rv: v8::ReturnValue<'_>,
1766  ) {
1767    let this = args.this();
1768    let Some(orig) = original_call_site(scope, this) else {
1769      return;
1770    };
1771    // `this[kOriginalCallsite].toString()`
1772    let Some(orig_to_string_v8) =
1773      call_method::<v8::String>(scope, orig, TO_STRING, &[])
1774    else {
1775      return;
1776    };
1777
1778    if let Some(v8_str) = to_string_inner(scope, this, orig, orig_to_string_v8)
1779    {
1780      rv.set(v8_str.into());
1781    } else {
1782      rv.set(orig_to_string_v8.into());
1783    }
1784  }
1785}
1786
1787/// Creates a template for a `Callsite`-like object, with
1788/// a patched `getFileName`.
1789/// Effectively:
1790/// ```js
1791/// const kOriginalCallsite = Symbol("_original");
1792/// {
1793///   [kOriginalCallsite]: originalCallSite,
1794///   getLineNumber() {
1795///     return this[kOriginalCallsite].getLineNumber();
1796///   },
1797///   // etc
1798///   getFileName() {
1799///     const fileName = this[kOriginalCallsite].getFileName();
1800///     return fileUrlToPath(fileName);
1801///   }
1802/// }
1803/// ```
1804pub(crate) fn make_callsite_prototype<'s, 'i>(
1805  scope: &mut v8::PinScope<'s, 'i>,
1806) -> v8::Local<'s, v8::Object> {
1807  let template = v8::ObjectTemplate::new(scope);
1808
1809  macro_rules! set_attr {
1810    ($scope:ident, $template:ident, $fn:ident, $field:ident) => {
1811      let key = $field.v8_string($scope).unwrap().into();
1812      $template.set_with_attr(
1813        key,
1814        v8::FunctionBuilder::<v8::FunctionTemplate>::new(callsite_fns::$fn)
1815          .build($scope)
1816          .into(),
1817        v8::PropertyAttribute::DONT_DELETE
1818          | v8::PropertyAttribute::DONT_ENUM
1819          | v8::PropertyAttribute::READ_ONLY,
1820      );
1821    };
1822  }
1823
1824  set_attr!(scope, template, get_this, GET_THIS);
1825  set_attr!(scope, template, get_type_name, GET_TYPE_NAME);
1826  set_attr!(scope, template, get_function, GET_FUNCTION);
1827  set_attr!(scope, template, get_function_name, GET_FUNCTION_NAME);
1828  set_attr!(scope, template, get_method_name, GET_METHOD_NAME);
1829  set_attr!(scope, template, get_file_name, GET_FILE_NAME);
1830  set_attr!(scope, template, get_line_number, GET_LINE_NUMBER);
1831  set_attr!(scope, template, get_column_number, GET_COLUMN_NUMBER);
1832  set_attr!(scope, template, get_eval_origin, GET_EVAL_ORIGIN);
1833  set_attr!(scope, template, is_toplevel, IS_TOPLEVEL);
1834  set_attr!(scope, template, is_eval, IS_EVAL);
1835  set_attr!(scope, template, is_native, IS_NATIVE);
1836  set_attr!(scope, template, is_constructor, IS_CONSTRUCTOR);
1837  set_attr!(scope, template, is_async, IS_ASYNC);
1838  set_attr!(scope, template, is_promise_all, IS_PROMISE_ALL);
1839  set_attr!(scope, template, get_promise_index, GET_PROMISE_INDEX);
1840  set_attr!(
1841    scope,
1842    template,
1843    get_script_name_or_source_url,
1844    GET_SCRIPT_NAME_OR_SOURCE_URL
1845  );
1846  set_attr!(scope, template, to_string, TO_STRING);
1847
1848  template.new_instance(scope).unwrap()
1849}
1850
1851#[inline(always)]
1852fn prepare_stack_trace_inner<'s, 'i, const PATCH_CALLSITES: bool>(
1853  scope: &mut v8::PinScope<'s, 'i>,
1854  error: v8::Local<'s, v8::Value>,
1855  callsites: v8::Local<'s, v8::Array>,
1856) -> v8::Local<'s, v8::Value> {
1857  // stash the callsites on the error object. this is used by `JsError::from_exception_inner`
1858  // to get more details on an error, because the v8 StackFrame API is missing some info.
1859  if let Ok(obj) = error.try_cast::<v8::Object>() {
1860    let key = call_site_evals_key(scope);
1861    obj.set_private(scope, key, callsites.into());
1862  }
1863
1864  // `globalThis.Error.prepareStackTrace`
1865  let global = scope.get_current_context().global(scope);
1866  let global_error =
1867    get_property(scope, global, ERROR).and_then(|g| g.try_cast().ok());
1868  let prepare_fn = global_error.and_then(|g| {
1869    get_property(scope, g, PREPARE_STACK_TRACE)
1870      .and_then(|f| f.try_cast::<v8::Function>().ok())
1871  });
1872
1873  // Note that the callback is called _instead_ of `Error.prepareStackTrace`
1874  // so we have to explicitly call the global function
1875  if let Some(prepare_fn) = prepare_fn {
1876    let callsites = if PATCH_CALLSITES {
1877      // User defined `Error.prepareStackTrace`.
1878      // Patch the callsites to have file paths, then call
1879      // the user's function
1880      let len = callsites.length();
1881      let mut patched = Vec::with_capacity(len as usize);
1882      let template = JsRuntime::state_from(scope)
1883        .callsite_prototype
1884        .borrow()
1885        .clone()
1886        .unwrap();
1887      let prototype = v8::Local::new(scope, template);
1888      for i in 0..len {
1889        let callsite =
1890          callsites.get_index(scope, i).unwrap().cast::<v8::Object>();
1891        patched.push(make_patched_callsite(scope, callsite, prototype).into());
1892      }
1893      v8::Array::new_with_elements(scope, &patched)
1894    } else {
1895      callsites
1896    };
1897
1898    // call the user's `prepareStackTrace` with our "callsite" objects
1899    let this = global_error.unwrap().into();
1900    let args = &[error, callsites.into()];
1901    return prepare_fn
1902      .call(scope, this, args)
1903      .unwrap_or_else(|| v8::undefined(scope).into());
1904  }
1905
1906  // no user defined `prepareStackTrace`, just call our default
1907  format_stack_trace(scope, error, callsites)
1908}
1909
1910/// Callback to prepare an error stack trace. This callback is invoked by V8 whenever a stack trace is created.
1911/// This will patch callsite objects to translate file URLs to file paths before they're
1912/// exposed to user-defined `prepareStackTrace` implementations.
1913///
1914/// This function is not used by default, but it can
1915/// be set directly on the [v8 isolate][v8::Isolate::set_prepare_stack_trace_callback]
1916pub fn prepare_stack_trace_callback_with_original_callsites<'s, 'i>(
1917  scope: &mut v8::PinScope<'s, 'i>,
1918  error: v8::Local<'s, v8::Value>,
1919  callsites: v8::Local<'s, v8::Array>,
1920) -> v8::Local<'s, v8::Value> {
1921  prepare_stack_trace_inner::<false>(scope, error, callsites)
1922}
1923
1924/// Callback to prepare an error stack trace. This callback is invoked by V8 whenever a stack trace is created.
1925/// This will patch callsite objects to translate file URLs to file paths before they're
1926/// exposed to user-defined `prepareStackTrace` implementations.
1927///
1928/// This function is the default callback, set on creating a `JsRuntime`, but the callback can also
1929/// be set directly on the [v8 isolate][v8::Isolate::set_prepare_stack_trace_callback]
1930pub fn prepare_stack_trace_callback<'s, 'i>(
1931  scope: &mut v8::PinScope<'s, 'i>,
1932  error: v8::Local<'s, v8::Value>,
1933  callsites: v8::Local<'s, v8::Array>,
1934) -> v8::Local<'s, v8::Value> {
1935  prepare_stack_trace_inner::<true>(scope, error, callsites)
1936}
1937
1938pub struct InitialCwd(pub Arc<Url>);
1939
1940pub fn format_stack_trace<'s, 'i>(
1941  scope: &mut v8::PinScope<'s, 'i>,
1942  error: v8::Local<'s, v8::Value>,
1943  callsites: v8::Local<'s, v8::Array>,
1944) -> v8::Local<'s, v8::Value> {
1945  let state = JsRuntime::state_from(scope);
1946  let maybe_initial_cwd = state
1947    .op_state
1948    .borrow()
1949    .try_borrow::<InitialCwd>()
1950    .map(|i| &i.0)
1951    .cloned();
1952  let mut result = String::new();
1953
1954  if let Ok(obj) = error.try_cast() {
1955    // Write out the error name + message, if any
1956    let msg = get_property(scope, obj, v8_static_strings::MESSAGE)
1957      .filter(|v| !v.is_undefined())
1958      .and_then(|v| v.to_string(scope))
1959      .map(|s| v8_to_rust_string(&s, scope))
1960      .unwrap_or_default();
1961    let name = get_property(scope, obj, v8_static_strings::NAME)
1962      .filter(|v| !v.is_undefined())
1963      .and_then(|v| v.to_string(scope))
1964      .map(|s| v8_to_rust_string(&s, scope))
1965      .unwrap_or_else(|| GENERIC_ERROR.to_string());
1966
1967    match (!msg.is_empty(), !name.is_empty()) {
1968      (true, true) => write!(result, "{}: {}", name, msg).unwrap(),
1969      (true, false) => write!(result, "{}", msg).unwrap(),
1970      (false, true) => write!(result, "{}", name).unwrap(),
1971      (false, false) => {}
1972    }
1973  }
1974
1975  // format each stack frame
1976  for i in 0..callsites.length() {
1977    let callsite = callsites.get_index(scope, i).unwrap().cast::<v8::Object>();
1978    v8::tc_scope!(let tc_scope, scope);
1979
1980    let Some(frame) = JsStackFrame::from_callsite_object(tc_scope, callsite)
1981    else {
1982      let exc = tc_scope
1983        .exception()
1984        .expect("JsStackFrame::from_callsite_object raised an exception");
1985      let message = match exc.to_string(tc_scope) {
1986        Some(s) => v8_to_rust_string(&s, tc_scope),
1987        None => String::new(),
1988      };
1989      #[allow(clippy::print_stderr, reason = "intentional warning output")]
1990      {
1991        eprintln!(
1992          "warning: Failed to create JsStackFrame from callsite object: {message}; Result so far: {result}. This is a bug in deno"
1993        );
1994      }
1995      break;
1996    };
1997    write!(
1998      result,
1999      "\n    at {}",
2000      format_frame::<NoAnsiColors>(&frame, maybe_initial_cwd.as_deref())
2001    )
2002    .unwrap();
2003  }
2004
2005  let result = v8::String::new(scope, &result).unwrap();
2006  result.into()
2007}
2008
2009/// Formats an error without using ansi colors.
2010pub struct NoAnsiColors;
2011
2012#[derive(Debug, Clone, Copy)]
2013/// Part of an error stack trace
2014pub enum ErrorElement {
2015  /// An anonymous file or method name
2016  Anonymous,
2017  /// Text signifying a native stack frame
2018  NativeFrame,
2019  /// A source line number
2020  LineNumber,
2021  /// A source column number
2022  ColumnNumber,
2023  /// The name of a function (or method)
2024  FunctionName,
2025  /// The name of a source file
2026  FileName,
2027  /// The path to the working directory, as in part of a file name
2028  WorkingDirPath,
2029  /// The origin of an error coming from `eval`ed code
2030  EvalOrigin,
2031  /// Text signifying a call to `Promise.all`
2032  PromiseAll,
2033  /// Other, plain text appearing in the error stack trace
2034  PlainText,
2035}
2036
2037/// Applies formatting to various parts of error stack traces.
2038///
2039/// This is to allow the `deno` crate to reuse `format_frame` and
2040/// `format_location` but apply ANSI colors for terminal output,
2041/// without adding extra dependencies to `deno_core`.
2042pub trait ErrorFormat {
2043  fn fmt_element(
2044    element: ErrorElement,
2045    in_extension_code: bool,
2046    s: &str,
2047  ) -> Cow<'_, str>;
2048}
2049
2050impl ErrorFormat for NoAnsiColors {
2051  fn fmt_element(
2052    _element: ErrorElement,
2053    _in_extension_code: bool,
2054    s: &str,
2055  ) -> Cow<'_, str> {
2056    s.into()
2057  }
2058}
2059
2060pub fn format_location<F: ErrorFormat>(
2061  frame: &JsStackFrame,
2062  maybe_initial_cwd: Option<&Url>,
2063) -> String {
2064  use ErrorElement::*;
2065  let in_extension_code = frame
2066    .file_name
2067    .as_ref()
2068    .map(|f| f.starts_with("ext:") || f.starts_with("node:"))
2069    .unwrap_or(false);
2070  if frame.is_native {
2071    return F::fmt_element(NativeFrame, in_extension_code, "native")
2072      .to_string();
2073  }
2074  let mut result = String::new();
2075  let file_name = frame.file_name.as_deref().unwrap_or("");
2076  if !file_name.is_empty() {
2077    let parts = format_file_name(file_name, maybe_initial_cwd);
2078    if let Some(working_dir_path) = &parts.working_dir_path {
2079      result +=
2080        &F::fmt_element(WorkingDirPath, in_extension_code, working_dir_path);
2081      result += &F::fmt_element(
2082        FileName,
2083        in_extension_code,
2084        parts.file_name.trim_start_matches("./"),
2085      )
2086    } else {
2087      result += &F::fmt_element(FileName, in_extension_code, &parts.file_name)
2088    }
2089  } else {
2090    if frame.is_eval {
2091      let eval_origin = frame.eval_origin.as_ref().unwrap();
2092      let formatted_eval_origin =
2093        format_eval_origin(eval_origin, maybe_initial_cwd);
2094      result +=
2095        &F::fmt_element(EvalOrigin, in_extension_code, &formatted_eval_origin);
2096      result += &F::fmt_element(PlainText, in_extension_code, ", ");
2097    }
2098    result += &F::fmt_element(Anonymous, in_extension_code, "<anonymous>");
2099  }
2100  if frame.is_wasm {
2101    // W3C WebAssembly Web API spec format:
2102    // {url}:wasm-function[{funcIndex}]:0x{pcOffset}
2103    // V8 returns function index (1-based) via getLineNumber()
2104    // and byte offset via getColumnNumber()
2105    if let Some(line_number) = frame.line_number {
2106      let func_index = line_number - 1;
2107      let wasm_func = format!("wasm-function[{func_index}]");
2108      result += &F::fmt_element(PlainText, in_extension_code, ":");
2109      result += &F::fmt_element(LineNumber, in_extension_code, &wasm_func);
2110    }
2111    if let Some(column_number) = frame.column_number {
2112      // V8's getColumnNumber() is 1-based, but wasm byte offsets are 0-based
2113      let pc_offset = format!("0x{:x}", column_number - 1);
2114      result += &F::fmt_element(PlainText, in_extension_code, ":");
2115      result += &F::fmt_element(ColumnNumber, in_extension_code, &pc_offset);
2116    }
2117  } else if let Some(line_number) = frame.line_number {
2118    result += &F::fmt_element(PlainText, in_extension_code, ":");
2119    result +=
2120      &F::fmt_element(LineNumber, in_extension_code, &line_number.to_string());
2121    if let Some(column_number) = frame.column_number {
2122      result += &F::fmt_element(PlainText, in_extension_code, ":");
2123      result += &F::fmt_element(
2124        ColumnNumber,
2125        in_extension_code,
2126        &column_number.to_string(),
2127      );
2128    }
2129  }
2130  result
2131}
2132
2133pub fn format_frame<F: ErrorFormat>(
2134  frame: &JsStackFrame,
2135  maybe_initial_cwd: Option<&Url>,
2136) -> String {
2137  use ErrorElement::*;
2138  let in_extension_code = frame
2139    .file_name
2140    .as_ref()
2141    .map(|f| f.starts_with("ext:") || f.starts_with("node:"))
2142    .unwrap_or(false);
2143  let is_method_call =
2144    !(frame.is_top_level.unwrap_or_default() || frame.is_constructor);
2145  let mut result = String::new();
2146  if frame.is_async {
2147    result += &F::fmt_element(PlainText, in_extension_code, "async ");
2148  }
2149  if frame.is_promise_all {
2150    result += &F::fmt_element(
2151      PromiseAll,
2152      in_extension_code,
2153      &format!(
2154        "Promise.all (index {})",
2155        frame.promise_index.unwrap_or_default()
2156      ),
2157    );
2158    return result;
2159  }
2160  if is_method_call {
2161    let mut formatted_method = String::new();
2162    if let Some(function_name) = &frame.function_name {
2163      if let Some(type_name) = &frame.type_name
2164        && !function_name.starts_with(type_name)
2165      {
2166        write!(formatted_method, "{type_name}.").unwrap();
2167      }
2168      formatted_method += function_name;
2169      if let Some(method_name) = &frame.method_name
2170        && !function_name.ends_with(method_name)
2171      {
2172        write!(formatted_method, " [as {method_name}]").unwrap();
2173      }
2174    } else {
2175      if let Some(type_name) = &frame.type_name {
2176        write!(formatted_method, "{type_name}.").unwrap();
2177      }
2178      if let Some(method_name) = &frame.method_name {
2179        formatted_method += method_name
2180      } else {
2181        formatted_method += "<anonymous>";
2182      }
2183    }
2184    result +=
2185      F::fmt_element(FunctionName, in_extension_code, &formatted_method)
2186        .as_ref();
2187  } else if frame.is_constructor {
2188    result += &F::fmt_element(PlainText, in_extension_code, "new ");
2189    if let Some(function_name) = &frame.function_name {
2190      write!(
2191        result,
2192        "{}",
2193        F::fmt_element(FunctionName, in_extension_code, function_name)
2194      )
2195      .unwrap();
2196    } else {
2197      result +=
2198        F::fmt_element(Anonymous, in_extension_code, "<anonymous>").as_ref();
2199    }
2200  } else if let Some(function_name) = &frame.function_name {
2201    result +=
2202      F::fmt_element(FunctionName, in_extension_code, function_name).as_ref();
2203  } else {
2204    result += &format_location::<F>(frame, maybe_initial_cwd);
2205    return result;
2206  }
2207  result += &F::fmt_element(PlainText, in_extension_code, " (");
2208  result += &format_location::<F>(frame, maybe_initial_cwd);
2209  result += &F::fmt_element(PlainText, in_extension_code, ")");
2210  result
2211}
2212
2213pub fn throw_error_one_byte_info(
2214  info: &v8::FunctionCallbackInfo,
2215  message: &str,
2216) {
2217  v8::callback_scope!(unsafe scope, info);
2218  throw_error_one_byte(scope, message);
2219}
2220
2221pub fn throw_error_js_error_class<'s, 'i>(
2222  scope: &mut v8::PinCallbackScope<'s, 'i>,
2223  err: &dyn JsErrorClass,
2224) {
2225  let exc = to_v8_error(scope, err);
2226  scope.throw_exception(exc);
2227}
2228
2229pub fn throw_error_one_byte<'s, 'i>(
2230  scope: &mut v8::PinCallbackScope<'s, 'i>,
2231  message: &str,
2232) {
2233  let msg = deno_core::v8::String::new_from_one_byte(
2234    scope,
2235    message.as_bytes(),
2236    deno_core::v8::NewStringType::Normal,
2237  )
2238  .unwrap();
2239  let exc = deno_core::v8::Exception::type_error(scope, msg);
2240  scope.throw_exception(exc);
2241}
2242
2243#[cfg(test)]
2244mod tests {
2245  use super::*;
2246
2247  #[test]
2248  fn test_format_file_name() {
2249    let file_name = format_file_name("data:,Hello%2C%20World%21", None);
2250    assert_eq!(file_name.file_name, "data:,Hello%2C%20World%21");
2251
2252    let too_long_name = "a".repeat(DATA_URL_ABBREV_THRESHOLD + 1);
2253    let file_name = format_file_name(
2254      &format!("data:text/plain;base64,{too_long_name}_%F0%9F%A6%95"),
2255      None,
2256    )
2257    .into_owned();
2258    let expected =
2259      "data:text/plain;base64,aaaaaaaaaaaaaaaaaaaa......aaaaaaa_%F0%9F%A6%95";
2260    assert_eq!(file_name.file_name, expected,);
2261    let file_name = format_file_name(
2262      &format!("data:text/plain;base64,{too_long_name}_%F0%9F%A6%95"),
2263      Some(&Url::parse("file:///foo").unwrap()),
2264    )
2265    .into_owned();
2266    assert_eq!(file_name.file_name, expected);
2267    let file_name = format_file_name("file:///foo/bar.ts", None);
2268    assert_eq!(file_name.file_name, "file:///foo/bar.ts");
2269
2270    let file_name = format_file_name(
2271      "file:///foo/bar.ts",
2272      Some(&Url::parse("file:///foo/").unwrap()),
2273    );
2274    assert_eq!(file_name.file_name, "./bar.ts");
2275
2276    let file_name =
2277      format_file_name("file:///%E6%9D%B1%E4%BA%AC/%F0%9F%A6%95.ts", None);
2278    assert_eq!(file_name.file_name, "file:///東京/🦕.ts");
2279  }
2280
2281  #[test]
2282  fn test_parse_eval_origin() {
2283    let cases = [
2284      (
2285        "eval at <anonymous> (file://path.ts:1:2)",
2286        Some(("eval at <anonymous> (", ("file://path.ts", 1, 2), ")")),
2287      ),
2288      (
2289        // malformed
2290        "eval at (s:1:2",
2291        None,
2292      ),
2293      (
2294        // malformed
2295        "at ()", None,
2296      ),
2297      (
2298        // url with parens
2299        "eval at foo (http://website.zzz/my-script).ts:1:2)",
2300        Some((
2301          "eval at foo (",
2302          ("http://website.zzz/my-script).ts", 1, 2),
2303          ")",
2304        )),
2305      ),
2306      (
2307        // nested
2308        "eval at foo (eval at bar (file://path.ts:1:2))",
2309        Some(("eval at foo (eval at bar (", ("file://path.ts", 1, 2), "))")),
2310      ),
2311    ];
2312    for (input, expect) in cases {
2313      match expect {
2314        Some((
2315          expect_before,
2316          (expect_file, expect_line, expect_col),
2317          expect_after,
2318        )) => {
2319          let (before, (file_name, line_number, column_number), after) =
2320            parse_eval_origin(input).unwrap();
2321          assert_eq!(before, expect_before);
2322          assert_eq!(file_name, expect_file);
2323          assert_eq!(line_number, expect_line);
2324          assert_eq!(column_number, expect_col);
2325          assert_eq!(after, expect_after);
2326        }
2327        None => {
2328          assert!(parse_eval_origin(input).is_none());
2329        }
2330      }
2331    }
2332  }
2333
2334  #[test]
2335  fn test_relative_specifier_within() {
2336    // File in the same directory - should return relative path
2337    let from = Url::parse("file:///Users/dev/project/").unwrap();
2338    let to = Url::parse("file:///Users/dev/project/foo.ts").unwrap();
2339    let result = relative_specifier_within(&from, &to);
2340    assert_eq!(result, Some("./foo.ts".to_string()));
2341
2342    // File in a subdirectory - should return relative path
2343    let from = Url::parse("file:///Users/dev/project/").unwrap();
2344    let to = Url::parse("file:///Users/dev/project/src/bar.ts").unwrap();
2345    let result = relative_specifier_within(&from, &to);
2346    assert_eq!(result, Some("./src/bar.ts".to_string()));
2347
2348    // File in a deeper subdirectory - should return relative path
2349    let from = Url::parse("file:///Users/dev/project/").unwrap();
2350    let to = Url::parse("file:///Users/dev/project/src/lib/utils.ts").unwrap();
2351    let result = relative_specifier_within(&from, &to);
2352    assert_eq!(result, Some("./src/lib/utils.ts".to_string()));
2353
2354    // File requires going up one level - should return None
2355    let from = Url::parse("file:///Users/dev/project/src/").unwrap();
2356    let to = Url::parse("file:///Users/dev/project/main.ts").unwrap();
2357    let result = relative_specifier_within(&from, &to);
2358    assert_eq!(result, None);
2359
2360    // File requires going up multiple levels - should return None
2361    let from = Url::parse("file:///Users/dev/project/src/lib/").unwrap();
2362    let to = Url::parse("file:///Users/dev/other/file.ts").unwrap();
2363    let result = relative_specifier_within(&from, &to);
2364    assert_eq!(result, None);
2365
2366    // Completely different path (e.g., bundled synthetic path) - should return None
2367    let from = Url::parse("file:///Users/dev/workspace/deno/").unwrap();
2368    let to = Url::parse("file:///a.ts").unwrap();
2369    let result = relative_specifier_within(&from, &to);
2370    assert_eq!(result, None);
2371
2372    // Same directory (edge case) - should return relative path
2373    let from = Url::parse("file:///Users/dev/project/").unwrap();
2374    let to = Url::parse("file:///Users/dev/project/").unwrap();
2375    let result = relative_specifier_within(&from, &to);
2376    assert_eq!(result, Some("./".to_string()));
2377
2378    // File in sibling directory - should return None
2379    let from = Url::parse("file:///Users/dev/project1/src/").unwrap();
2380    let to = Url::parse("file:///Users/dev/project2/foo.ts").unwrap();
2381    let result = relative_specifier_within(&from, &to);
2382    assert_eq!(result, None);
2383
2384    // HTTP URLs (not file://) - should work if in same path
2385    let from = Url::parse("http://example.com/app/").unwrap();
2386    let to = Url::parse("http://example.com/app/main.js").unwrap();
2387    let result = relative_specifier_within(&from, &to);
2388    assert_eq!(result, Some("./main.js".to_string()));
2389
2390    // HTTP URLs requiring going up - should return None
2391    let from = Url::parse("http://example.com/app/src/").unwrap();
2392    let to = Url::parse("http://example.com/app/main.js").unwrap();
2393    let result = relative_specifier_within(&from, &to);
2394    assert_eq!(result, None);
2395  }
2396
2397  #[cfg(not(miri))]
2398  #[test]
2399  fn test_to_v8_error_handles_null_builder_exception() {
2400    let mut runtime = JsRuntime::new(Default::default());
2401
2402    deno_core::scope!(scope, runtime);
2403
2404    let throw_null_fn: v8::Local<v8::Function> =
2405      JsRuntime::eval(scope, "(function () { throw null; })").unwrap();
2406
2407    // Override custom error builder so it throws null
2408    let exception_state = JsRealm::exception_state_from_scope(scope);
2409    exception_state
2410      .js_build_custom_error_cb
2411      .borrow_mut()
2412      .replace(v8::Global::new(scope, throw_null_fn));
2413
2414    let err = CoreErrorKind::TLA;
2415    let expected_message = err.get_message();
2416
2417    let value = to_v8_error(scope, &err);
2418    let value: v8::Local<v8::String> = value
2419      .try_into()
2420      .expect("should fall back to message string");
2421
2422    assert_eq!(value.to_rust_string_lossy(scope), expected_message);
2423  }
2424
2425  #[cfg(not(miri))]
2426  #[test]
2427  fn test_to_v8_error_handles_stack_overflow_in_builder() {
2428    let mut runtime = JsRuntime::new(Default::default());
2429
2430    deno_core::scope!(scope, runtime);
2431
2432    // Simulate the error builder throwing RangeError (as happens during
2433    // stack overflow when the builder callback itself exceeds the call
2434    // stack limit).
2435    let throw_range_error_fn: v8::Local<v8::Function> = JsRuntime::eval(
2436      scope,
2437      "(function () { throw new RangeError('Maximum call stack size exceeded'); })",
2438    )
2439    .unwrap();
2440
2441    let exception_state = JsRealm::exception_state_from_scope(scope);
2442    exception_state
2443      .js_build_custom_error_cb
2444      .borrow_mut()
2445      .replace(v8::Global::new(scope, throw_range_error_fn));
2446
2447    let err = CoreErrorKind::TLA;
2448    let expected_message = err.get_message();
2449
2450    // Should not panic, should fall back to the message string
2451    let value = to_v8_error(scope, &err);
2452    let value: v8::Local<v8::String> = value
2453      .try_into()
2454      .expect("should fall back to message string");
2455
2456    assert_eq!(value.to_rust_string_lossy(scope), expected_message);
2457  }
2458}