@@ -11,6 +11,8 @@ use deno_core::error::format_frame;
1111use deno_core:: url:: Url ;
1212use deno_terminal:: colors;
1313
14+ use crate :: source_highlight:: syntax_highlight_source_line;
15+
1416#[ derive( Debug , Clone ) ]
1517struct ErrorReference < ' a > {
1618 from : & ' a JsError ,
@@ -108,6 +110,7 @@ impl deno_core::error::ErrorFormat for AnsiColors {
108110fn format_maybe_source_line (
109111 source_line : Option < & str > ,
110112 column_number : Option < i64 > ,
113+ line_number : Option < i64 > ,
111114 is_error : bool ,
112115 level : usize ,
113116) -> String {
@@ -136,6 +139,25 @@ fn format_maybe_source_line(
136139 ) ;
137140 }
138141
142+ // Build the line number gutter prefix: " 42 | "
143+ let ( line_prefix, gutter_width) = if let Some ( ln) = line_number {
144+ let ln_str = ln. to_string ( ) ;
145+ let prefix = if colors:: use_color ( ) {
146+ format ! ( "{} {} " , colors:: gray( & ln_str) , colors:: gray( "|" ) )
147+ } else {
148+ format ! ( "{} | " , ln_str)
149+ } ;
150+ // Width of the gutter in visible characters (for the caret alignment)
151+ let width = ln_str. len ( ) + 3 ; // "42 | " = digits + " | "
152+ ( prefix, width)
153+ } else {
154+ ( String :: new ( ) , 0 )
155+ } ;
156+
157+ // Build caret padding (accounting for gutter width)
158+ for _ in 0 ..gutter_width {
159+ s. push ( ' ' ) ;
160+ }
139161 for _i in 0 ..( column_number - 1 ) {
140162 if source_line. chars ( ) . nth ( _i as usize ) . unwrap ( ) == '\t' {
141163 s. push ( '\t' ) ;
@@ -145,14 +167,18 @@ fn format_maybe_source_line(
145167 }
146168 s. push ( '^' ) ;
147169 let color_underline = if is_error {
148- colors:: red ( & s) . to_string ( )
170+ colors:: red_bold ( & s) . to_string ( )
149171 } else {
150172 colors:: cyan ( & s) . to_string ( )
151173 } ;
152174
153175 let indent = format ! ( "{:indent$}" , "" , indent = level) ;
176+ let highlighted_source =
177+ syntax_highlight_source_line ( source_line, colors:: use_color ( ) ) ;
154178
155- format ! ( "\n {indent}{source_line}\n {indent}{color_underline}" )
179+ format ! (
180+ "\n {indent}{line_prefix}{highlighted_source}\n {indent}{color_underline}"
181+ )
156182}
157183
158184fn find_recursive_cause ( js_error : & JsError ) -> Option < ErrorReference < ' _ > > {
@@ -220,6 +246,55 @@ fn stack_frame_is_ext(frame: &deno_core::error::JsStackFrame) -> bool {
220246 . unwrap_or ( false )
221247}
222248
249+ /// Colorize an exception message like "Uncaught (in promise) TypeError: msg".
250+ ///
251+ /// - "(in promise)" is grayed out
252+ /// - The error class name (e.g. "TypeError") is colored red
253+ fn colorize_exception_message ( msg : & str ) -> String {
254+ if !colors:: use_color ( ) {
255+ return msg. to_string ( ) ;
256+ }
257+
258+ let mut remaining = msg;
259+ let mut result = String :: with_capacity ( msg. len ( ) + 40 ) ;
260+
261+ // Strip "Uncaught " prefix (will be re-added plain)
262+ let uncaught = remaining. starts_with ( "Uncaught " ) ;
263+ if uncaught {
264+ result. push_str ( "Uncaught " ) ;
265+ remaining = & remaining[ "Uncaught " . len ( ) ..] ;
266+ }
267+
268+ // Handle optional "(in promise) "
269+ if remaining. starts_with ( "(in promise) " ) {
270+ result. push_str ( & colors:: gray ( "(in promise)" ) . to_string ( ) ) ;
271+ result. push ( ' ' ) ;
272+ remaining = & remaining[ "(in promise) " . len ( ) ..] ;
273+ }
274+
275+ // Find the error class name — everything up to the first ": "
276+ if let Some ( colon_pos) = remaining. find ( ": " ) {
277+ let class_name = & remaining[ ..colon_pos] ;
278+ // Only colorize if it looks like an error class name
279+ // (starts with uppercase, contains only alphanumeric chars)
280+ if class_name
281+ . chars ( )
282+ . next ( )
283+ . is_some_and ( |c| c. is_ascii_uppercase ( ) )
284+ && class_name. chars ( ) . all ( |c| c. is_ascii_alphanumeric ( ) )
285+ {
286+ result. push_str ( & colors:: red_bold ( class_name) . to_string ( ) ) ;
287+ result. push_str ( & remaining[ colon_pos..] ) ;
288+ } else {
289+ result. push_str ( remaining) ;
290+ }
291+ } else {
292+ result. push_str ( remaining) ;
293+ }
294+
295+ result
296+ }
297+
223298fn format_js_error_inner (
224299 js_error : & JsError ,
225300 circular : Option < IndexedErrorReference > ,
@@ -230,7 +305,7 @@ fn format_js_error_inner(
230305) -> String {
231306 let mut s = String :: new ( ) ;
232307
233- s. push_str ( & js_error. exception_message ) ;
308+ s. push_str ( & colorize_exception_message ( & js_error. exception_message ) ) ;
234309
235310 if let Some ( circular) = & circular
236311 && js_error. is_same_error ( circular. reference . to )
@@ -252,16 +327,19 @@ fn format_js_error_inner(
252327 s. push_str ( & aggregated_message) ;
253328 }
254329
255- let column_number = js_error
330+ let source_frame = js_error
256331 . source_line_frame_index
257- . and_then ( |i| js_error. frames . get ( i) . unwrap ( ) . column_number ) ;
332+ . and_then ( |i| js_error. frames . get ( i) ) ;
333+ let column_number = source_frame. and_then ( |f| f. column_number ) ;
334+ let line_number = source_frame. and_then ( |f| f. line_number ) ;
258335 s. push_str ( & format_maybe_source_line (
259336 if include_source_code {
260337 js_error. source_line . as_deref ( )
261338 } else {
262339 None
263340 } ,
264341 column_number,
342+ line_number,
265343 true ,
266344 0 ,
267345 ) ) ;
@@ -529,17 +607,89 @@ mod tests {
529607
530608 #[ test]
531609 fn test_format_none_source_line ( ) {
532- let actual = format_maybe_source_line ( None , None , false , 0 ) ;
610+ let actual = format_maybe_source_line ( None , None , None , false , 0 ) ;
533611 assert_eq ! ( actual, "" ) ;
534612 }
535613
536614 #[ test]
537615 fn test_format_some_source_line ( ) {
538- let actual =
539- format_maybe_source_line ( Some ( "console.log('foo');" ) , Some ( 9 ) , true , 0 ) ;
616+ let actual = format_maybe_source_line (
617+ Some ( "console.log('foo');" ) ,
618+ Some ( 9 ) ,
619+ None ,
620+ true ,
621+ 0 ,
622+ ) ;
540623 assert_eq ! (
541624 strip_ansi_codes( & actual) ,
542625 "\n console.log(\' foo\' );\n ^"
543626 ) ;
544627 }
628+
629+ #[ test]
630+ fn test_format_source_line_with_line_number ( ) {
631+ let actual = format_maybe_source_line (
632+ Some ( "console.log('foo');" ) ,
633+ Some ( 9 ) ,
634+ Some ( 42 ) ,
635+ true ,
636+ 0 ,
637+ ) ;
638+ let stripped = strip_ansi_codes ( & actual) ;
639+ assert_eq ! ( stripped, "\n 42 | console.log(\' foo\' );\n ^" ) ;
640+ }
641+
642+ #[ test]
643+ fn test_colorize_exception_message_no_color ( ) {
644+ colors:: set_use_color ( false ) ;
645+ let msg = "Uncaught (in promise) TypeError: foo" ;
646+ assert_eq ! ( colorize_exception_message( msg) , msg) ;
647+ colors:: set_use_color ( true ) ;
648+ }
649+
650+ #[ test]
651+ fn test_colorize_exception_message_basic ( ) {
652+ colors:: set_use_color ( true ) ;
653+ let result =
654+ colorize_exception_message ( "Uncaught TypeError: something failed" ) ;
655+ let stripped = strip_ansi_codes ( & result) ;
656+ assert_eq ! ( stripped, "Uncaught TypeError: something failed" ) ;
657+ // "TypeError" should be red+bold
658+ assert ! ( result. contains( "TypeError" ) , "result: {result}" ) ;
659+ // Should NOT contain plain "Uncaught TypeError" (TypeError must be styled)
660+ assert ! ( !result. contains( "Uncaught TypeError:" ) , "result: {result}" ) ;
661+ }
662+
663+ #[ test]
664+ fn test_colorize_exception_message_in_promise ( ) {
665+ colors:: set_use_color ( true ) ;
666+ let result = colorize_exception_message (
667+ "Uncaught (in promise) Error: something failed" ,
668+ ) ;
669+ let stripped = strip_ansi_codes ( & result) ;
670+ assert_eq ! ( stripped, "Uncaught (in promise) Error: something failed" ) ;
671+ // "(in promise)" should be styled (gray)
672+ assert ! (
673+ !result. contains( "Uncaught (in promise) Error" ) ,
674+ "result: {result}"
675+ ) ;
676+ }
677+
678+ #[ test]
679+ fn test_colorize_exception_message_no_colon ( ) {
680+ colors:: set_use_color ( true ) ;
681+ // No ": " in message — should pass through unchanged
682+ let result = colorize_exception_message ( "Uncaught something" ) ;
683+ let stripped = strip_ansi_codes ( & result) ;
684+ assert_eq ! ( stripped, "Uncaught something" ) ;
685+ }
686+
687+ #[ test]
688+ fn test_colorize_exception_message_not_class_name ( ) {
689+ colors:: set_use_color ( true ) ;
690+ // lowercase after "Uncaught " — not an error class name
691+ let result = colorize_exception_message ( "Uncaught error: something failed" ) ;
692+ let stripped = strip_ansi_codes ( & result) ;
693+ assert_eq ! ( stripped, "Uncaught error: something failed" ) ;
694+ }
545695}
0 commit comments