deno_runtime/
fmt_errors.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2//! This mod provides DenoError to unify errors across Deno.
3use color_print::cformat;
4use color_print::cstr;
5use deno_core::error::format_frame;
6use deno_core::error::JsError;
7use deno_terminal::colors;
8use std::fmt::Write as _;
9
10#[derive(Debug, Clone)]
11struct ErrorReference<'a> {
12  from: &'a JsError,
13  to: &'a JsError,
14}
15
16#[derive(Debug, Clone)]
17struct IndexedErrorReference<'a> {
18  reference: ErrorReference<'a>,
19  index: usize,
20}
21
22#[derive(Debug)]
23enum FixSuggestionKind {
24  Info,
25  Hint,
26  Docs,
27}
28
29#[derive(Debug)]
30enum FixSuggestionMessage<'a> {
31  Single(&'a str),
32  Multiline(&'a [&'a str]),
33}
34
35#[derive(Debug)]
36pub struct FixSuggestion<'a> {
37  kind: FixSuggestionKind,
38  message: FixSuggestionMessage<'a>,
39}
40
41impl<'a> FixSuggestion<'a> {
42  pub fn info(message: &'a str) -> Self {
43    Self {
44      kind: FixSuggestionKind::Info,
45      message: FixSuggestionMessage::Single(message),
46    }
47  }
48
49  pub fn info_multiline(messages: &'a [&'a str]) -> Self {
50    Self {
51      kind: FixSuggestionKind::Info,
52      message: FixSuggestionMessage::Multiline(messages),
53    }
54  }
55
56  pub fn hint(message: &'a str) -> Self {
57    Self {
58      kind: FixSuggestionKind::Hint,
59      message: FixSuggestionMessage::Single(message),
60    }
61  }
62
63  pub fn hint_multiline(messages: &'a [&'a str]) -> Self {
64    Self {
65      kind: FixSuggestionKind::Hint,
66      message: FixSuggestionMessage::Multiline(messages),
67    }
68  }
69
70  pub fn docs(url: &'a str) -> Self {
71    Self {
72      kind: FixSuggestionKind::Docs,
73      message: FixSuggestionMessage::Single(url),
74    }
75  }
76}
77
78struct AnsiColors;
79
80impl deno_core::error::ErrorFormat for AnsiColors {
81  fn fmt_element(
82    element: deno_core::error::ErrorElement,
83    s: &str,
84  ) -> std::borrow::Cow<'_, str> {
85    use deno_core::error::ErrorElement::*;
86    match element {
87      Anonymous | NativeFrame | FileName | EvalOrigin => {
88        colors::cyan(s).to_string().into()
89      }
90      LineNumber | ColumnNumber => colors::yellow(s).to_string().into(),
91      FunctionName | PromiseAll => colors::italic_bold(s).to_string().into(),
92    }
93  }
94}
95
96/// Take an optional source line and associated information to format it into
97/// a pretty printed version of that line.
98fn format_maybe_source_line(
99  source_line: Option<&str>,
100  column_number: Option<i64>,
101  is_error: bool,
102  level: usize,
103) -> String {
104  if source_line.is_none() || column_number.is_none() {
105    return "".to_string();
106  }
107
108  let source_line = source_line.unwrap();
109  // sometimes source_line gets set with an empty string, which then outputs
110  // an empty source line when displayed, so need just short circuit here.
111  if source_line.is_empty() {
112    return "".to_string();
113  }
114  if source_line.contains("Couldn't format source line: ") {
115    return format!("\n{source_line}");
116  }
117
118  let mut s = String::new();
119  let column_number = column_number.unwrap();
120
121  if column_number as usize > source_line.len() {
122    return format!(
123      "\n{} Couldn't format source line: Column {} is out of bounds (source may have changed at runtime)",
124      colors::yellow("Warning"), column_number,
125    );
126  }
127
128  for _i in 0..(column_number - 1) {
129    if source_line.chars().nth(_i as usize).unwrap() == '\t' {
130      s.push('\t');
131    } else {
132      s.push(' ');
133    }
134  }
135  s.push('^');
136  let color_underline = if is_error {
137    colors::red(&s).to_string()
138  } else {
139    colors::cyan(&s).to_string()
140  };
141
142  let indent = format!("{:indent$}", "", indent = level);
143
144  format!("\n{indent}{source_line}\n{indent}{color_underline}")
145}
146
147fn find_recursive_cause(js_error: &JsError) -> Option<ErrorReference> {
148  let mut history = Vec::<&JsError>::new();
149
150  let mut current_error: &JsError = js_error;
151
152  while let Some(cause) = &current_error.cause {
153    history.push(current_error);
154
155    if let Some(seen) = history.iter().find(|&el| cause.is_same_error(el)) {
156      return Some(ErrorReference {
157        from: current_error,
158        to: seen,
159      });
160    } else {
161      current_error = cause;
162    }
163  }
164
165  None
166}
167
168fn format_aggregated_error(
169  aggregated_errors: &Vec<JsError>,
170  circular_reference_index: usize,
171) -> String {
172  let mut s = String::new();
173  let mut nested_circular_reference_index = circular_reference_index;
174
175  for js_error in aggregated_errors {
176    let aggregated_circular = find_recursive_cause(js_error);
177    if aggregated_circular.is_some() {
178      nested_circular_reference_index += 1;
179    }
180    let error_string = format_js_error_inner(
181      js_error,
182      aggregated_circular.map(|reference| IndexedErrorReference {
183        reference,
184        index: nested_circular_reference_index,
185      }),
186      false,
187      vec![],
188    );
189
190    for line in error_string.trim_start_matches("Uncaught ").lines() {
191      write!(s, "\n    {line}").unwrap();
192    }
193  }
194
195  s
196}
197
198fn format_js_error_inner(
199  js_error: &JsError,
200  circular: Option<IndexedErrorReference>,
201  include_source_code: bool,
202  suggestions: Vec<FixSuggestion>,
203) -> String {
204  let mut s = String::new();
205
206  s.push_str(&js_error.exception_message);
207
208  if let Some(circular) = &circular {
209    if js_error.is_same_error(circular.reference.to) {
210      write!(s, " {}", colors::cyan(format!("<ref *{}>", circular.index)))
211        .unwrap();
212    }
213  }
214
215  if let Some(aggregated) = &js_error.aggregated {
216    let aggregated_message = format_aggregated_error(
217      aggregated,
218      circular
219        .as_ref()
220        .map(|circular| circular.index)
221        .unwrap_or(0),
222    );
223    s.push_str(&aggregated_message);
224  }
225
226  let column_number = js_error
227    .source_line_frame_index
228    .and_then(|i| js_error.frames.get(i).unwrap().column_number);
229  s.push_str(&format_maybe_source_line(
230    if include_source_code {
231      js_error.source_line.as_deref()
232    } else {
233      None
234    },
235    column_number,
236    true,
237    0,
238  ));
239  for frame in &js_error.frames {
240    write!(s, "\n    at {}", format_frame::<AnsiColors>(frame)).unwrap();
241  }
242  if let Some(cause) = &js_error.cause {
243    let is_caused_by_circular = circular
244      .as_ref()
245      .map(|circular| js_error.is_same_error(circular.reference.from))
246      .unwrap_or(false);
247
248    let error_string = if is_caused_by_circular {
249      colors::cyan(format!("[Circular *{}]", circular.unwrap().index))
250        .to_string()
251    } else {
252      format_js_error_inner(cause, circular, false, vec![])
253    };
254
255    write!(
256      s,
257      "\nCaused by: {}",
258      error_string.trim_start_matches("Uncaught ")
259    )
260    .unwrap();
261  }
262  if !suggestions.is_empty() {
263    write!(s, "\n\n").unwrap();
264    for (index, suggestion) in suggestions.iter().enumerate() {
265      write!(s, "    ").unwrap();
266      match suggestion.kind {
267        FixSuggestionKind::Hint => {
268          write!(s, "{} ", colors::cyan("hint:")).unwrap()
269        }
270        FixSuggestionKind::Info => {
271          write!(s, "{} ", colors::yellow("info:")).unwrap()
272        }
273        FixSuggestionKind::Docs => {
274          write!(s, "{} ", colors::green("docs:")).unwrap()
275        }
276      };
277      match suggestion.message {
278        FixSuggestionMessage::Single(msg) => {
279          if matches!(suggestion.kind, FixSuggestionKind::Docs) {
280            write!(s, "{}", cformat!("<u>{}</>", msg)).unwrap();
281          } else {
282            write!(s, "{}", msg).unwrap();
283          }
284        }
285        FixSuggestionMessage::Multiline(messages) => {
286          for (idx, message) in messages.iter().enumerate() {
287            if idx != 0 {
288              writeln!(s).unwrap();
289              write!(s, "          ").unwrap();
290            }
291            write!(s, "{}", message).unwrap();
292          }
293        }
294      }
295
296      if index != (suggestions.len() - 1) {
297        writeln!(s).unwrap();
298      }
299    }
300  }
301
302  s
303}
304
305fn get_suggestions_for_terminal_errors(e: &JsError) -> Vec<FixSuggestion> {
306  if let Some(msg) = &e.message {
307    if msg.contains("module is not defined")
308      || msg.contains("exports is not defined")
309      || msg.contains("require is not defined")
310    {
311      return vec![
312        FixSuggestion::info_multiline(&[
313          cstr!("Deno supports CommonJS modules in <u>.cjs</> files, or when the closest"),
314          cstr!("<u>package.json</> has a <i>\"type\": \"commonjs\"</> option.")
315        ]),
316        FixSuggestion::hint_multiline(&[
317          "Rewrite this module to ESM,",
318          cstr!("or change the file extension to <u>.cjs</u>,"),
319          cstr!("or add <u>package.json</> next to the file with <i>\"type\": \"commonjs\"</> option,"),
320          cstr!("or pass <i>--unstable-detect-cjs</> flag to detect CommonJS when loading."),
321        ]),
322        FixSuggestion::docs("https://docs.deno.com/go/commonjs"),
323      ];
324    } else if msg.contains("__filename is not defined") {
325      return vec![
326        FixSuggestion::info(cstr!(
327          "<u>__filename</> global is not available in ES modules."
328        )),
329        FixSuggestion::hint(cstr!("Use <u>import.meta.filename</> instead.")),
330      ];
331    } else if msg.contains("__dirname is not defined") {
332      return vec![
333        FixSuggestion::info(cstr!(
334          "<u>__dirname</> global is not available in ES modules."
335        )),
336        FixSuggestion::hint(cstr!("Use <u>import.meta.dirname</> instead.")),
337      ];
338    } else if msg.contains("Buffer is not defined") {
339      return vec![
340        FixSuggestion::info(cstr!(
341          "<u>Buffer</> is not available in the global scope in Deno."
342        )),
343        FixSuggestion::hint_multiline(&[
344          cstr!("Import it explicitly with <u>import { Buffer } from \"node:buffer\";</>,"),
345          cstr!("or run again with <u>--unstable-node-globals</> flag to add this global."),
346        ]),
347      ];
348    } else if msg.contains("clearImmediate is not defined") {
349      return vec![
350        FixSuggestion::info(cstr!(
351          "<u>clearImmediate</> is not available in the global scope in Deno."
352        )),
353        FixSuggestion::hint_multiline(&[
354          cstr!("Import it explicitly with <u>import { clearImmediate } from \"node:timers\";</>,"),
355          cstr!("or run again with <u>--unstable-node-globals</> flag to add this global."),
356        ]),
357      ];
358    } else if msg.contains("setImmediate is not defined") {
359      return vec![
360        FixSuggestion::info(cstr!(
361          "<u>setImmediate</> is not available in the global scope in Deno."
362        )),
363        FixSuggestion::hint_multiline(
364          &[cstr!("Import it explicitly with <u>import { setImmediate } from \"node:timers\";</>,"),
365          cstr!("or run again with <u>--unstable-node-globals</> flag to add this global."),
366        ]),
367      ];
368    } else if msg.contains("global is not defined") {
369      return vec![
370        FixSuggestion::info(cstr!(
371          "<u>global</> is not available in the global scope in Deno."
372        )),
373        FixSuggestion::hint_multiline(&[
374          cstr!("Use <u>globalThis</> instead, or assign <u>globalThis.global = globalThis</>,"),
375          cstr!("or run again with <u>--unstable-node-globals</> flag to add this global."),
376        ]),
377      ];
378    } else if msg.contains("openKv is not a function") {
379      return vec![
380        FixSuggestion::info("Deno.openKv() is an unstable API."),
381        FixSuggestion::hint(
382          "Run again with `--unstable-kv` flag to enable this API.",
383        ),
384      ];
385    } else if msg.contains("cron is not a function") {
386      return vec![
387        FixSuggestion::info("Deno.cron() is an unstable API."),
388        FixSuggestion::hint(
389          "Run again with `--unstable-cron` flag to enable this API.",
390        ),
391      ];
392    } else if msg.contains("WebSocketStream is not defined") {
393      return vec![
394        FixSuggestion::info("new WebSocketStream() is an unstable API."),
395        FixSuggestion::hint(
396          "Run again with `--unstable-net` flag to enable this API.",
397        ),
398      ];
399    } else if msg.contains("Temporal is not defined") {
400      return vec![
401        FixSuggestion::info("Temporal is an unstable API."),
402        FixSuggestion::hint(
403          "Run again with `--unstable-temporal` flag to enable this API.",
404        ),
405      ];
406    } else if msg.contains("BroadcastChannel is not defined") {
407      return vec![
408        FixSuggestion::info("BroadcastChannel is an unstable API."),
409        FixSuggestion::hint(
410          "Run again with `--unstable-broadcast-channel` flag to enable this API.",
411        ),
412      ];
413    } else if msg.contains("window is not defined") {
414      return vec![
415        FixSuggestion::info("window global is not available in Deno 2."),
416        FixSuggestion::hint("Replace `window` with `globalThis`."),
417      ];
418    } else if msg.contains("UnsafeWindowSurface is not a constructor") {
419      return vec![
420        FixSuggestion::info("Deno.UnsafeWindowSurface is an unstable API."),
421        FixSuggestion::hint(
422          "Run again with `--unstable-webgpu` flag to enable this API.",
423        ),
424      ];
425    // Try to capture errors like:
426    // ```
427    // Uncaught Error: Cannot find module '../build/Release/canvas.node'
428    // Require stack:
429    // - /.../deno/npm/registry.npmjs.org/canvas/2.11.2/lib/bindings.js
430    // - /.../.cache/deno/npm/registry.npmjs.org/canvas/2.11.2/lib/canvas.js
431    // ```
432    } else if msg.contains("Cannot find module")
433      && msg.contains("Require stack")
434      && msg.contains(".node'")
435    {
436      return vec![
437        FixSuggestion::info_multiline(
438          &[
439            "Trying to execute an npm package using Node-API addons,",
440            "these packages require local `node_modules` directory to be present."
441          ]
442        ),
443        FixSuggestion::hint_multiline(
444          &[
445            "Add `\"nodeModulesDir\": \"auto\" option to `deno.json`, and then run",
446            "`deno install --allow-scripts=npm:<package> --entrypoint <script>` to setup `node_modules` directory."
447          ]
448        )
449      ];
450    } else if msg.contains("document is not defined") {
451      return vec![
452        FixSuggestion::info(cstr!(
453          "<u>document</> global is not available in Deno."
454        )),
455        FixSuggestion::hint_multiline(&[
456          cstr!("Use a library like <u>happy-dom</>, <u>deno_dom</>, <u>linkedom</> or <u>JSDom</>"),
457          cstr!("and setup the <u>document</> global according to the library documentation."),
458        ]),
459      ];
460    }
461  }
462
463  vec![]
464}
465
466/// Format a [`JsError`] for terminal output.
467pub fn format_js_error(js_error: &JsError) -> String {
468  let circular =
469    find_recursive_cause(js_error).map(|reference| IndexedErrorReference {
470      reference,
471      index: 1,
472    });
473  let suggestions = get_suggestions_for_terminal_errors(js_error);
474  format_js_error_inner(js_error, circular, true, suggestions)
475}
476
477#[cfg(test)]
478mod tests {
479  use super::*;
480  use test_util::strip_ansi_codes;
481
482  #[test]
483  fn test_format_none_source_line() {
484    let actual = format_maybe_source_line(None, None, false, 0);
485    assert_eq!(actual, "");
486  }
487
488  #[test]
489  fn test_format_some_source_line() {
490    let actual =
491      format_maybe_source_line(Some("console.log('foo');"), Some(9), true, 0);
492    assert_eq!(
493      strip_ansi_codes(&actual),
494      "\nconsole.log(\'foo\');\n        ^"
495    );
496  }
497}