1use 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
17pub 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
24pub 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#[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
93pub 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#[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 #[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 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 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 }
267
268impl JsError {
269 pub fn is_same_error(&self, other: &JsError) -> bool {
275 let a = self;
276 let b = other;
277 a.name == b.name
280 && a.message == b.message
281 && a.stack == b.stack
282 && (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 let scope = &mut v8::HandleScope::new(scope);
306
307 let exception_message = msg.get(scope).to_rust_string_lossy(scope);
308
309 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 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 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 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 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 let frames_v8 = get_property(scope, exception, "__callSiteEvals");
414 let frames_v8: Option<v8::Local<v8::Array>> =
416 frames_v8.and_then(|a| a.try_into().ok());
417
418 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 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 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 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
534pub(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
563pub(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
597pub(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
632pub(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 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 scope.set_microtasks_policy(v8::MicrotasksPolicy::Explicit);
691 scope.cancel_terminate_execution();
696 let exception = if let Some(dispatched_exception) =
697 state.get_dispatched_exception_as_local(scope)
698 {
699 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 let message = v8::String::new(scope, "execution terminated").unwrap();
711 v8::Exception::error(scope, message)
712 } else {
713 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 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}