rw_deno_core/
error.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3use std::borrow::Cow;
4use std::collections::HashSet;
5use std::fmt;
6use std::fmt::Debug;
7use std::fmt::Display;
8use std::fmt::Formatter;
9
10use anyhow::Error;
11
12use crate::runtime::JsRealm;
13use crate::runtime::JsRuntime;
14use crate::source_map::SourceMapApplication;
15use crate::url::Url;
16
17/// A generic wrapper that can encapsulate any concrete error type.
18// TODO(ry) Deprecate AnyError and encourage deno_core::anyhow::Error instead.
19pub type AnyError = anyhow::Error;
20
21pub type JsErrorCreateFn = dyn Fn(JsError) -> Error;
22pub type GetErrorClassFn = &'static dyn for<'e> Fn(&'e Error) -> &'static str;
23
24/// Creates a new error with a caller-specified error class name and message.
25pub fn custom_error(
26  class: &'static str,
27  message: impl Into<Cow<'static, str>>,
28) -> Error {
29  CustomError {
30    class,
31    message: message.into(),
32  }
33  .into()
34}
35
36pub fn generic_error(message: impl Into<Cow<'static, str>>) -> Error {
37  custom_error("Error", message)
38}
39
40pub fn type_error(message: impl Into<Cow<'static, str>>) -> Error {
41  custom_error("TypeError", message)
42}
43
44pub fn range_error(message: impl Into<Cow<'static, str>>) -> Error {
45  custom_error("RangeError", message)
46}
47
48pub fn invalid_hostname(hostname: &str) -> Error {
49  type_error(format!("Invalid hostname: '{hostname}'"))
50}
51
52pub fn uri_error(message: impl Into<Cow<'static, str>>) -> Error {
53  custom_error("URIError", message)
54}
55
56pub fn bad_resource(message: impl Into<Cow<'static, str>>) -> Error {
57  custom_error("BadResource", message)
58}
59
60pub fn bad_resource_id() -> Error {
61  custom_error("BadResource", "Bad resource ID")
62}
63
64pub fn not_supported() -> Error {
65  custom_error("NotSupported", "The operation is not supported")
66}
67
68pub fn resource_unavailable() -> Error {
69  custom_error(
70    "Busy",
71    "Resource is unavailable because it is in use by a promise",
72  )
73}
74
75/// A simple error type that lets the creator specify both the error message and
76/// the error class name. This type is private; externally it only ever appears
77/// wrapped in an `anyhow::Error`. To retrieve the error class name from a wrapped
78/// `CustomError`, use the function `get_custom_error_class()`.
79#[derive(Debug)]
80struct CustomError {
81  class: &'static str,
82  message: Cow<'static, str>,
83}
84
85impl Display for CustomError {
86  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
87    f.write_str(&self.message)
88  }
89}
90
91impl std::error::Error for CustomError {}
92
93/// If this error was crated with `custom_error()`, return the specified error
94/// class name. In all other cases this function returns `None`.
95pub fn get_custom_error_class(error: &Error) -> Option<&'static str> {
96  error.downcast_ref::<CustomError>().map(|e| e.class)
97}
98
99pub fn to_v8_error<'a>(
100  scope: &mut v8::HandleScope<'a>,
101  get_class: GetErrorClassFn,
102  error: &Error,
103) -> v8::Local<'a, v8::Value> {
104  let tc_scope = &mut v8::TryCatch::new(scope);
105  let cb = JsRealm::exception_state_from_scope(tc_scope)
106    .js_build_custom_error_cb
107    .borrow()
108    .clone()
109    .expect("Custom error builder must be set");
110  let cb = cb.open(tc_scope);
111  let this = v8::undefined(tc_scope).into();
112  let class = v8::String::new(tc_scope, get_class(error)).unwrap();
113  let message = v8::String::new(tc_scope, &format!("{error:#}")).unwrap();
114  let mut args = vec![class.into(), message.into()];
115  if let Some(code) = crate::error_codes::get_error_code(error) {
116    args.push(v8::String::new(tc_scope, code).unwrap().into());
117  }
118  let maybe_exception = cb.call(tc_scope, this, &args);
119
120  match maybe_exception {
121    Some(exception) => exception,
122    None => {
123      let mut msg =
124        "Custom error class must have a builder registered".to_string();
125      if tc_scope.has_caught() {
126        let e = tc_scope.exception().unwrap();
127        let js_error = JsError::from_v8_exception(tc_scope, e);
128        msg = format!("{}: {}", msg, js_error.exception_message);
129      }
130      panic!("{}", msg);
131    }
132  }
133}
134
135/// A `JsError` represents an exception coming from V8, with stack frames and
136/// line numbers. The deno_cli crate defines another `JsError` type, which wraps
137/// the one defined here, that adds source map support and colorful formatting.
138/// When updating this struct, also update errors_are_equal_without_cause() in
139/// fmt_error.rs.
140#[derive(Debug, PartialEq, Clone, serde::Deserialize, serde::Serialize)]
141#[serde(rename_all = "camelCase")]
142pub struct JsError {
143  pub name: Option<String>,
144  pub message: Option<String>,
145  pub stack: Option<String>,
146  pub cause: Option<Box<JsError>>,
147  pub exception_message: String,
148  pub frames: Vec<JsStackFrame>,
149  pub source_line: Option<String>,
150  pub source_line_frame_index: Option<usize>,
151  pub aggregated: Option<Vec<JsError>>,
152}
153
154#[derive(Debug, Eq, PartialEq, Clone, serde::Deserialize, serde::Serialize)]
155#[serde(rename_all = "camelCase")]
156pub struct JsStackFrame {
157  pub type_name: Option<String>,
158  pub function_name: Option<String>,
159  pub method_name: Option<String>,
160  pub file_name: Option<String>,
161  pub line_number: Option<i64>,
162  pub column_number: Option<i64>,
163  pub eval_origin: Option<String>,
164  // Warning! isToplevel has inconsistent snake<>camel case, "typo" originates in v8:
165  // https://source.chromium.org/search?q=isToplevel&sq=&ss=chromium%2Fchromium%2Fsrc:v8%2F
166  #[serde(rename = "isToplevel")]
167  pub is_top_level: Option<bool>,
168  pub is_eval: bool,
169  pub is_native: bool,
170  pub is_constructor: bool,
171  pub is_async: bool,
172  pub is_promise_all: bool,
173  pub promise_index: Option<i64>,
174}
175
176impl JsStackFrame {
177  pub fn from_location(
178    file_name: Option<String>,
179    line_number: Option<i64>,
180    column_number: Option<i64>,
181  ) -> Self {
182    Self {
183      type_name: None,
184      function_name: None,
185      method_name: None,
186      file_name,
187      line_number,
188      column_number,
189      eval_origin: None,
190      is_top_level: None,
191      is_eval: false,
192      is_native: false,
193      is_constructor: false,
194      is_async: false,
195      is_promise_all: false,
196      promise_index: None,
197    }
198  }
199
200  /// Gets the source mapped stack frame corresponding to the
201  /// (script_resource_name, line_number, column_number) from a v8 message.
202  /// For non-syntax errors, it should also correspond to the first stack frame.
203  pub fn from_v8_message<'a>(
204    scope: &'a mut v8::HandleScope,
205    message: v8::Local<'a, v8::Message>,
206  ) -> Option<Self> {
207    let f = message.get_script_resource_name(scope)?;
208    let f: v8::Local<v8::String> = f.try_into().ok()?;
209    let f = f.to_rust_string_lossy(scope);
210    let l = message.get_line_number(scope)? as u32;
211    // V8's column numbers are 0-based, we want 1-based.
212    let c = message.get_start_column() as u32 + 1;
213    let state = JsRuntime::state_from(scope);
214    let mut source_mapper = state.source_mapper.borrow_mut();
215    match source_mapper.apply_source_map(&f, l, c) {
216      SourceMapApplication::Unchanged => Some(JsStackFrame::from_location(
217        Some(f),
218        Some(l.into()),
219        Some(c.into()),
220      )),
221      SourceMapApplication::LineAndColumn {
222        line_number,
223        column_number,
224      } => Some(JsStackFrame::from_location(
225        Some(f),
226        Some(line_number.into()),
227        Some(column_number.into()),
228      )),
229      SourceMapApplication::LineAndColumnAndFileName {
230        file_name,
231        line_number,
232        column_number,
233      } => Some(JsStackFrame::from_location(
234        Some(file_name),
235        Some(line_number.into()),
236        Some(column_number.into()),
237      )),
238    }
239  }
240
241  pub fn maybe_format_location(&self) -> Option<String> {
242    Some(format!(
243      "{}:{}:{}",
244      self.file_name.as_ref()?,
245      self.line_number?,
246      self.column_number?
247    ))
248  }
249}
250
251fn get_property<'a>(
252  scope: &mut v8::HandleScope<'a>,
253  object: v8::Local<v8::Object>,
254  key: &str,
255) -> Option<v8::Local<'a, v8::Value>> {
256  let key = v8::String::new(scope, key).unwrap();
257  object.get(scope, key.into())
258}
259
260#[derive(Default, serde::Deserialize)]
261pub(crate) struct NativeJsError {
262  pub name: Option<String>,
263  pub message: Option<String>,
264  // Warning! .stack is special so handled by itself
265  // stack: Option<String>,
266}
267
268impl JsError {
269  /// Compares all properties of JsError, except for JsError::cause. This function is used to
270  /// detect that 2 JsError objects in a JsError::cause chain are identical, ie. there is a recursive cause.
271  ///
272  /// We don't have access to object identity here, so we do it via field comparison. Ideally this should
273  /// be able to maintain object identity somehow.
274  pub fn is_same_error(&self, other: &JsError) -> bool {
275    let a = self;
276    let b = other;
277    // `a.cause == b.cause` omitted, because it is absent in recursive errors,
278    // despite the error being identical to a previously seen one.
279    a.name == b.name
280      && a.message == b.message
281      && a.stack == b.stack
282      // TODO(mmastrac): we need consistency around when we insert "in promise" and when we don't. For now, we
283      // are going to manually replace this part of the string.
284      && (a.exception_message == b.exception_message
285        || a.exception_message.replace(" (in promise) ", " ") == b.exception_message.replace(" (in promise) ", " "))
286      && a.frames == b.frames
287      && a.source_line == b.source_line
288      && a.source_line_frame_index == b.source_line_frame_index
289      && a.aggregated == b.aggregated
290  }
291
292  pub fn from_v8_exception(
293    scope: &mut v8::HandleScope,
294    exception: v8::Local<v8::Value>,
295  ) -> Self {
296    Self::inner_from_v8_exception(scope, exception, Default::default())
297  }
298
299  pub fn from_v8_message<'a>(
300    scope: &'a mut v8::HandleScope,
301    msg: v8::Local<'a, v8::Message>,
302  ) -> Self {
303    // Create a new HandleScope because we're creating a lot of new local
304    // handles below.
305    let scope = &mut v8::HandleScope::new(scope);
306
307    let exception_message = msg.get(scope).to_rust_string_lossy(scope);
308
309    // Convert them into Vec<JsStackFrame>
310    let mut frames: Vec<JsStackFrame> = vec![];
311    let mut source_line = None;
312    let mut source_line_frame_index = None;
313
314    if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) {
315      frames = vec![stack_frame];
316    }
317    {
318      let state = JsRuntime::state_from(scope);
319      let mut source_mapper = state.source_mapper.borrow_mut();
320      for (i, frame) in frames.iter().enumerate() {
321        if let (Some(file_name), Some(line_number)) =
322          (&frame.file_name, frame.line_number)
323        {
324          if !file_name.trim_start_matches('[').starts_with("ext:") {
325            source_line = source_mapper.get_source_line(file_name, line_number);
326            source_line_frame_index = Some(i);
327            break;
328          }
329        }
330      }
331    }
332
333    Self {
334      name: None,
335      message: None,
336      exception_message,
337      cause: None,
338      source_line,
339      source_line_frame_index,
340      frames,
341      stack: None,
342      aggregated: None,
343    }
344  }
345
346  fn inner_from_v8_exception<'a>(
347    scope: &'a mut v8::HandleScope,
348    exception: v8::Local<'a, v8::Value>,
349    mut seen: HashSet<v8::Local<'a, v8::Object>>,
350  ) -> Self {
351    // Create a new HandleScope because we're creating a lot of new local
352    // handles below.
353    let scope = &mut v8::HandleScope::new(scope);
354
355    let msg = v8::Exception::create_message(scope, exception);
356
357    let mut exception_message = None;
358    let exception_state = JsRealm::exception_state_from_scope(scope);
359
360    let js_format_exception_cb =
361      exception_state.js_format_exception_cb.borrow().clone();
362    if let Some(format_exception_cb) = js_format_exception_cb {
363      let format_exception_cb = format_exception_cb.open(scope);
364      let this = v8::undefined(scope).into();
365      let formatted = format_exception_cb.call(scope, this, &[exception]);
366      if let Some(formatted) = formatted {
367        if formatted.is_string() {
368          exception_message = Some(formatted.to_rust_string_lossy(scope));
369        }
370      }
371    }
372
373    if is_instance_of_error(scope, exception) {
374      let v8_exception = exception;
375      // The exception is a JS Error object.
376      let exception: v8::Local<v8::Object> = exception.try_into().unwrap();
377      let cause = get_property(scope, exception, "cause");
378      let e: NativeJsError =
379        serde_v8::from_v8(scope, exception.into()).unwrap_or_default();
380      // Get the message by formatting error.name and error.message.
381      let name = e.name.clone().unwrap_or_else(|| "Error".to_string());
382      let message_prop = e.message.clone().unwrap_or_default();
383      let exception_message = exception_message.unwrap_or_else(|| {
384        if !name.is_empty() && !message_prop.is_empty() {
385          format!("Uncaught {name}: {message_prop}")
386        } else if !name.is_empty() {
387          format!("Uncaught {name}")
388        } else if !message_prop.is_empty() {
389          format!("Uncaught {message_prop}")
390        } else {
391          "Uncaught".to_string()
392        }
393      });
394      let cause = cause.and_then(|cause| {
395        if cause.is_undefined() || seen.contains(&exception) {
396          None
397        } else {
398          seen.insert(exception);
399          Some(Box::new(JsError::inner_from_v8_exception(
400            scope, cause, seen,
401          )))
402        }
403      });
404
405      // Access error.stack to ensure that prepareStackTrace() has been called.
406      // This should populate error.__callSiteEvals.
407      let stack = get_property(scope, exception, "stack");
408      let stack: Option<v8::Local<v8::String>> =
409        stack.and_then(|s| s.try_into().ok());
410      let stack = stack.map(|s| s.to_rust_string_lossy(scope));
411
412      // Read an array of structured frames from error.__callSiteEvals.
413      let frames_v8 = get_property(scope, exception, "__callSiteEvals");
414      // Ignore non-array values
415      let frames_v8: Option<v8::Local<v8::Array>> =
416        frames_v8.and_then(|a| a.try_into().ok());
417
418      // Convert them into Vec<JsStackFrame>
419      let mut frames: Vec<JsStackFrame> = match frames_v8 {
420        Some(frames_v8) => serde_v8::from_v8(scope, frames_v8.into()).unwrap(),
421        None => vec![],
422      };
423      let mut source_line = None;
424      let mut source_line_frame_index = None;
425
426      // When the stack frame array is empty, but the source location given by
427      // (script_resource_name, line_number, start_column + 1) exists, this is
428      // likely a syntax error. For the sake of formatting we treat it like it
429      // was given as a single stack frame.
430      if frames.is_empty() {
431        if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) {
432          frames = vec![stack_frame];
433        }
434      }
435      {
436        let state = JsRuntime::state_from(scope);
437        let mut source_mapper = state.source_mapper.borrow_mut();
438        if source_mapper.has_user_sources() {
439          for (i, frame) in frames.iter().enumerate() {
440            if let (Some(file_name), Some(line_number)) =
441              (&frame.file_name, frame.line_number)
442            {
443              if !file_name.trim_start_matches('[').starts_with("ext:") {
444                source_line =
445                  source_mapper.get_source_line(file_name, line_number);
446                source_line_frame_index = Some(i);
447                break;
448              }
449            }
450          }
451        } else if let Some(frame) = frames.first() {
452          if let Some(file_name) = &frame.file_name {
453            if !file_name.trim_start_matches('[').starts_with("ext:") {
454              source_line = msg
455                .get_source_line(scope)
456                .map(|v| v.to_rust_string_lossy(scope));
457              source_line_frame_index = Some(0);
458            }
459          }
460        }
461      }
462
463      let mut aggregated: Option<Vec<JsError>> = None;
464      if is_aggregate_error(scope, v8_exception) {
465        // Read an array of stored errors, this is only defined for `AggregateError`
466        let aggregated_errors = get_property(scope, exception, "errors");
467        let aggregated_errors: Option<v8::Local<v8::Array>> =
468          aggregated_errors.and_then(|a| a.try_into().ok());
469
470        if let Some(errors) = aggregated_errors {
471          if errors.length() > 0 {
472            let mut agg = vec![];
473            for i in 0..errors.length() {
474              let error = errors.get_index(scope, i).unwrap();
475              let js_error = Self::from_v8_exception(scope, error);
476              agg.push(js_error);
477            }
478            aggregated = Some(agg);
479          }
480        }
481      };
482
483      Self {
484        name: e.name,
485        message: e.message,
486        exception_message,
487        cause,
488        source_line,
489        source_line_frame_index,
490        frames,
491        stack,
492        aggregated,
493      }
494    } else {
495      let exception_message = exception_message
496        .unwrap_or_else(|| msg.get(scope).to_rust_string_lossy(scope));
497      // The exception is not a JS Error object.
498      // Get the message given by V8::Exception::create_message(), and provide
499      // empty frames.
500      Self {
501        name: None,
502        message: None,
503        exception_message,
504        cause: None,
505        source_line: None,
506        source_line_frame_index: None,
507        frames: vec![],
508        stack: None,
509        aggregated: None,
510      }
511    }
512  }
513}
514
515impl std::error::Error for JsError {}
516
517impl Display for JsError {
518  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
519    if let Some(stack) = &self.stack {
520      let stack_lines = stack.lines();
521      if stack_lines.count() > 1 {
522        return write!(f, "{stack}");
523      }
524    }
525    write!(f, "{}", self.exception_message)?;
526    let location = self.frames.first().and_then(|f| f.maybe_format_location());
527    if let Some(location) = location {
528      write!(f, "\n    at {location}")?;
529    }
530    Ok(())
531  }
532}
533
534// TODO(piscisaureus): rusty_v8 should implement the Error trait on
535// values of type v8::Global<T>.
536pub(crate) fn to_v8_type_error(
537  scope: &mut v8::HandleScope,
538  err: Error,
539) -> v8::Global<v8::Value> {
540  let err_string = err.to_string();
541  let error_chain = err
542    .chain()
543    .skip(1)
544    .filter(|e| e.to_string() != err_string)
545    .map(|e| e.to_string())
546    .collect::<Vec<_>>();
547
548  let message = if !error_chain.is_empty() {
549    format!(
550      "{}\n  Caused by:\n    {}",
551      err_string,
552      error_chain.join("\n    ")
553    )
554  } else {
555    err_string
556  };
557
558  let message = v8::String::new(scope, &message).unwrap();
559  let exception = v8::Exception::type_error(scope, message);
560  v8::Global::new(scope, exception)
561}
562
563/// Implements `value instanceof primordials.Error` in JS. Similar to
564/// `Value::is_native_error()` but more closely matches the semantics
565/// of `instanceof`. `Value::is_native_error()` also checks for static class
566/// inheritance rather than just scanning the prototype chain, which doesn't
567/// work with our WebIDL implementation of `DOMException`.
568pub(crate) fn is_instance_of_error(
569  scope: &mut v8::HandleScope,
570  value: v8::Local<v8::Value>,
571) -> bool {
572  if !value.is_object() {
573    return false;
574  }
575  let message = v8::String::empty(scope);
576  let error_prototype = v8::Exception::error(scope, message)
577    .to_object(scope)
578    .unwrap()
579    .get_prototype(scope)
580    .unwrap();
581  let mut maybe_prototype =
582    value.to_object(scope).unwrap().get_prototype(scope);
583  while let Some(prototype) = maybe_prototype {
584    if !prototype.is_object() {
585      return false;
586    }
587    if prototype.strict_equals(error_prototype) {
588      return true;
589    }
590    maybe_prototype = prototype
591      .to_object(scope)
592      .and_then(|o| o.get_prototype(scope));
593  }
594  false
595}
596
597/// Implements `value instanceof primordials.AggregateError` in JS,
598/// by walking the prototype chain, and comparing each links constructor `name` property.
599///
600/// NOTE: There is currently no way to detect `AggregateError` via `rusty_v8`,
601/// as v8 itself doesn't expose `v8__Exception__AggregateError`,
602/// and we cannot create bindings for it. This forces us to rely on `name` inference.
603pub(crate) fn is_aggregate_error(
604  scope: &mut v8::HandleScope,
605  value: v8::Local<v8::Value>,
606) -> bool {
607  let mut maybe_prototype = Some(value);
608  while let Some(prototype) = maybe_prototype {
609    if !prototype.is_object() {
610      return false;
611    }
612
613    let prototype = prototype.to_object(scope).unwrap();
614    let prototype_name = match get_property(scope, prototype, "constructor") {
615      Some(constructor) => {
616        let ctor = constructor.to_object(scope).unwrap();
617        get_property(scope, ctor, "name").map(|v| v.to_rust_string_lossy(scope))
618      }
619      None => return false,
620    };
621
622    if prototype_name == Some(String::from("AggregateError")) {
623      return true;
624    }
625
626    maybe_prototype = prototype.get_prototype(scope);
627  }
628
629  false
630}
631
632/// Check if the error has a proper stack trace. The stack trace checked is the
633/// one passed to `prepareStackTrace()`, not `msg.get_stack_trace()`.
634pub(crate) fn has_call_site(
635  scope: &mut v8::HandleScope,
636  exception: v8::Local<v8::Value>,
637) -> bool {
638  if !exception.is_object() {
639    return false;
640  }
641  let exception = exception.to_object(scope).unwrap();
642  // Access error.stack to ensure that prepareStackTrace() has been called.
643  // This should populate error.__callSiteEvals.
644  get_property(scope, exception, "stack");
645  let frames_v8 = get_property(scope, exception, "__callSiteEvals");
646  let frames_v8: Option<v8::Local<v8::Array>> =
647    frames_v8.and_then(|a| a.try_into().ok());
648  if let Some(frames_v8) = frames_v8 {
649    if frames_v8.length() > 0 {
650      return true;
651    }
652  }
653  false
654}
655
656const DATA_URL_ABBREV_THRESHOLD: usize = 150;
657
658pub fn format_file_name(file_name: &str) -> String {
659  abbrev_file_name(file_name).unwrap_or_else(|| file_name.to_string())
660}
661
662fn abbrev_file_name(file_name: &str) -> Option<String> {
663  if file_name.len() <= DATA_URL_ABBREV_THRESHOLD {
664    return None;
665  }
666  let url = Url::parse(file_name).ok()?;
667  if url.scheme() != "data" {
668    return None;
669  }
670  let (head, tail) = url.path().split_once(',')?;
671  let len = tail.len();
672  let start = tail.get(0..20)?;
673  let end = tail.get(len - 20..)?;
674  Some(format!("{}:{},{}......{}", url.scheme(), head, start, end))
675}
676
677pub(crate) fn exception_to_err_result<T>(
678  scope: &mut v8::HandleScope,
679  exception: v8::Local<v8::Value>,
680  mut in_promise: bool,
681  clear_error: bool,
682) -> Result<T, Error> {
683  let state = JsRealm::exception_state_from_scope(scope);
684
685  let mut was_terminating_execution = scope.is_execution_terminating();
686
687  // Disable running microtasks for a moment. When upgrading to V8 v11.4
688  // we discovered that canceling termination here will cause the queued
689  // microtasks to run which breaks some tests.
690  scope.set_microtasks_policy(v8::MicrotasksPolicy::Explicit);
691  // If TerminateExecution was called, cancel isolate termination so that the
692  // exception can be created. Note that `scope.is_execution_terminating()` may
693  // have returned false if TerminateExecution was indeed called but there was
694  // no JS to execute after the call.
695  scope.cancel_terminate_execution();
696  let exception = if let Some(dispatched_exception) =
697    state.get_dispatched_exception_as_local(scope)
698  {
699    // If termination is the result of a `reportUnhandledException` call, we want
700    // to use the exception that was passed to it rather than the exception that
701    // was passed to this function.
702    in_promise = state.is_dispatched_exception_promise();
703    if clear_error {
704      state.clear_error();
705      was_terminating_execution = false;
706    }
707    dispatched_exception
708  } else if was_terminating_execution && exception.is_null_or_undefined() {
709    // If we are terminating and there is no exception, throw `new Error("execution terminated")``.
710    let message = v8::String::new(scope, "execution terminated").unwrap();
711    v8::Exception::error(scope, message)
712  } else {
713    // Otherwise re-use the exception
714    exception
715  };
716
717  let mut js_error = JsError::from_v8_exception(scope, exception);
718  if in_promise {
719    js_error.exception_message = format!(
720      "Uncaught (in promise) {}",
721      js_error.exception_message.trim_start_matches("Uncaught ")
722    );
723  }
724
725  if was_terminating_execution {
726    // Resume exception termination.
727    scope.terminate_execution();
728  }
729  scope.set_microtasks_policy(v8::MicrotasksPolicy::Auto);
730
731  Err(js_error.into())
732}
733
734pub fn throw_type_error(scope: &mut v8::HandleScope, message: impl AsRef<str>) {
735  let message = v8::String::new(scope, message.as_ref()).unwrap();
736  let exception = v8::Exception::type_error(scope, message);
737  scope.throw_exception(exception);
738}
739
740#[cfg(test)]
741mod tests {
742  use super::*;
743
744  #[test]
745  fn test_bad_resource() {
746    let err = bad_resource("Resource has been closed");
747    assert_eq!(err.to_string(), "Resource has been closed");
748  }
749
750  #[test]
751  fn test_bad_resource_id() {
752    let err = bad_resource_id();
753    assert_eq!(err.to_string(), "Bad resource ID");
754  }
755}