Repository: nushell/nushell (language: rust)

Issue:
`error make` ability to reference caller span
### Related problem

When throwing an error via `error make` in a custom command, the default span points to the `error make`.  It would be much more user-friendly (and useful) if there was some way to point it to the command itself.

Example:

```nu
def problem [] {
  let error_condition = true
  if $error_condition {
    error make {
      msg: "Oops"
    }
  }
}
```

```
> problem
Error:   × Oops
   ╭─[entry #14:4:5]
 3 │   if $error_condition {
 4 │     error make {
   ·     ─────┬────
   ·          ╰── originates from here
 5 │       msg: "Oops"
   ╰────
```

### Describe the solution you'd like

Some way in the `error make` to specify that the span should be the calling custom command.  Preferably also some way to point to its input or arguments as the culprit.

E.g.:

```nu
def problem [] {
  let error_condition = true
  if $error_condition {
    error make {
      msg: "Oops"
      source: "caller" # and/or perhaps "pipeline" or "arguments"
    }
  }
}
```

### Describe alternatives you've considered

I can obtain the *entire* caller via `view files | last`, but this isn't helpful if the previous "file" called the command multiple times.

### Additional context and details

_No response_


Discussion / hints:
Found this searching the same issue. I am brand new to this so could be wrong but currently, it seems you can reference some arguments:

```
export def "dep link" [ 
        target: string # The target project. One of: cgsweb | map
        --name (-n): string # Name of the package. Ex: @cgsweb/core
        --do-remove (-x) # perform the pnpm remove step in the target project.
    ] {
        if (($do_remove) and ($name == null)) {
            error make {
                msg: "--name (-n) must be specified for package to remove in --do-remove (-x)"
                label: {
                    text: "Require --name",
                    span: (metadata $target).span
                }
            }
        }
```

Causing this error results in the target positional arg to correctly be identified. 
```
❯ : dep link somthing -x
Error:   × --name (-n) must be specified for package to remove in --do-remove (-x)
   ╭─[entry #99:1:10]
 1 │ dep link somthing -x
   ·          ────┬───
   ·              ╰── Require --name
   ╰────
```

However, if I try to use a non-positional arg (--something or -t) it now just uses the command name and not the argument
```
        if (($do_remove) and ($name == null)) {
            error make {
                msg: "--name (-n) must be specified for package to remove in --do-remove (-x)"
                label: {
                    text: "Require --name",
                    span: (metadata $do_remove).span
                }
            }
        }
```

```
❯ : dep link somthing -x
Error:   × --name (-n) must be specified for package to remove in --do-remove (-x)
   ╭─[entry #101:1:1]
 1 │ dep link somthing -x
   · ────┬───
   ·     ╰── Require --name
   ╰────

```

It would also be nice to span a "ghost" of an arg that is missing in my case, to clearly indicate that you should have an argument there. 

Fix patch:
diff --git a/crates/nu-engine/src/eval_ir.rs b/crates/nu-engine/src/eval_ir.rs
index ecdebbac51780..25b4cc1921de1 100644
--- a/crates/nu-engine/src/eval_ir.rs
+++ b/crates/nu-engine/src/eval_ir.rs
@@ -187,6 +187,7 @@ fn eval_ir_block_impl<D: DebugContext>(
 
     // Program counter, starts at zero.
     let mut pc = 0;
+    let need_backtrace = ctx.engine_state.get_env_var("NU_BACKTRACE").is_some();
 
     while pc < ir_block.instructions.len() {
         let instruction = &ir_block.instructions[pc];
@@ -195,7 +196,7 @@ fn eval_ir_block_impl<D: DebugContext>(
 
         D::enter_instruction(ctx.engine_state, ir_block, pc, ctx.registers);
 
-        let result = eval_instruction::<D>(ctx, instruction, span, ast);
+        let result = eval_instruction::<D>(ctx, instruction, span, ast, need_backtrace);
 
         D::leave_instruction(
             ctx.engine_state,
@@ -228,8 +229,10 @@ fn eval_ir_block_impl<D: DebugContext>(
                     // If an error handler is set, branch there
                     prepare_error_handler(ctx, error_handler, Some(err.into_spanned(*span)));
                     pc = error_handler.handler_index;
+                } else if need_backtrace {
+                    let err = ShellError::into_chainned(err, *span);
+                    return Err(err);
                 } else {
-                    // If not, exit the block with the error
                     return Err(err);
                 }
             }
@@ -285,6 +288,7 @@ fn eval_instruction<D: DebugContext>(
     instruction: &Instruction,
     span: &Span,
     ast: &Option<IrAstRef>,
+    need_backtrace: bool,
 ) -> Result<InstructionResult, ShellError> {
     use self::InstructionResult::*;
 
@@ -548,7 +552,14 @@ fn eval_instruction<D: DebugContext>(
         }
         Instruction::Call { decl_id, src_dst } => {
             let input = ctx.take_reg(*src_dst);
-            let result = eval_call::<D>(ctx, *decl_id, *span, input)?;
+            let mut result = eval_call::<D>(ctx, *decl_id, *span, input)?;
+            if need_backtrace {
+                match &mut result {
+                    PipelineData::ByteStream(s, ..) => s.push_caller_span(*span),
+                    PipelineData::ListStream(s, ..) => s.push_caller_span(*span),
+                    _ => (),
+                };
+            }
             ctx.put_reg(*src_dst, result);
             Ok(Continue)
         }
@@ -1457,14 +1468,40 @@ fn drain(ctx: &mut EvalContext<'_>, data: PipelineData) -> Result<InstructionRes
     match data {
         PipelineData::ByteStream(stream, ..) => {
             let span = stream.span();
-            if let Err(err) = stream.drain() {
+            let callback_spans = stream.get_caller_spans().clone();
+            if let Err(mut err) = stream.drain() {
                 ctx.stack.set_last_error(&err);
-                return Err(err);
+                if callback_spans.is_empty() {
+                    return Err(err);
+                } else {
+                    for s in callback_spans {
+                        err = ShellError::EvalBlockWithInput {
+                            span: s,
+                            sources: vec![err],
+                        }
+                    }
+                    return Err(err);
+                }
             } else {
                 ctx.stack.set_last_exit_code(0, span);
             }
         }
-        PipelineData::ListStream(stream, ..) => stream.drain()?,
+        PipelineData::ListStream(stream, ..) => {
+            let callback_spans = stream.get_caller_spans().clone();
+            if let Err(mut err) = stream.drain() {
+                if callback_spans.is_empty() {
+                    return Err(err);
+                } else {
+                    for s in callback_spans {
+                        err = ShellError::EvalBlockWithInput {
+                            span: s,
+                            sources: vec![err],
+                        }
+                    }
+                    return Err(err);
+                }
+            }
+        }
         PipelineData::Value(..) | PipelineData::Empty => {}
     }
     Ok(Continue)
diff --git a/crates/nu-protocol/src/errors/chained_error.rs b/crates/nu-protocol/src/errors/chained_error.rs
new file mode 100644
index 0000000000000..ad45c441578a8
--- /dev/null
+++ b/crates/nu-protocol/src/errors/chained_error.rs
@@ -0,0 +1,120 @@
+use super::shell_error::ShellError;
+use crate::Span;
+use miette::{LabeledSpan, Severity, SourceCode};
+use thiserror::Error;
+
+/// An error struct that contains source errors.
+///
+/// However, it's a bit special; if the error is constructed for the first time using
+/// [`ChainedError::new`], it will behave the same as the single source error.
+///
+/// If it's constructed nestedly using [`ChainedError::new_chained`], it will treat all underlying errors as related.
+///
+/// For a usage example, please check [`ShellError::into_chainned`].
+#[derive(Debug, Clone, PartialEq, Error)]
+pub struct ChainedError {
+    first: bool,
+    pub(crate) sources: Vec<ShellError>,
+    span: Span,
+}
+
+impl std::fmt::Display for ChainedError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if self.first {
+            write!(f, "{}", self.sources[0])
+        } else {
+            write!(f, "oops")
+        }
+    }
+}
+
+impl ChainedError {
+    pub fn new(source: ShellError, span: Span) -> Self {
+        Self {
+            first: true,
+            sources: vec![source],
+            span,
+        }
+    }
+
+    pub fn new_chained(sources: Self, span: Span) -> Self {
+        Self {
+            first: false,
+            sources: vec![ShellError::ChainedError(sources)],
+            span,
+        }
+    }
+}
+
+impl miette::Diagnostic for ChainedError {
+    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
+        if self.first {
+            self.sources[0].related()
+        } else {
+            Some(Box::new(self.sources.iter().map(|s| s as _)))
+        }
+    }
+
+    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
+        if self.first {
+            self.sources[0].code()
+        } else {
+            Some(Box::new("chained_error"))
+        }
+    }
+
+    fn severity(&self) -> Option<Severity> {
+        if self.first {
+            self.sources[0].severity()
+        } else {
+            None
+        }
+    }
+
+    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
+        if self.first {
+            self.sources[0].help()
+        } else {
+            None
+        }
+    }
+
+    fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
+        if self.first {
+            self.sources[0].url()
+        } else {
+            None
+        }
+    }
+
+    fn labels<'a>(&'a self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + 'a>> {
+        if self.first {
+            self.sources[0].labels()
+        } else {
+            Some(Box::new(
+                vec![LabeledSpan::new_with_span(
+                    Some("error happened when running this".to_string()),
+                    self.span,
+                )]
+                .into_iter(),
+            ))
+        }
+    }
+
+    // Finally, we redirect the source_code method to our own source.
+    fn source_code(&self) -> Option<&dyn SourceCode> {
+        if self.first {
+            self.sources[0].source_code()
+        } else {
+            None
+        }
+    }
+
+    fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> {
+        if self.first {
+            self.sources[0].diagnostic_source()
+        } else {
+            None
+        }
+    }
+}
diff --git a/crates/nu-protocol/src/errors/cli_error.rs b/crates/nu-protocol/src/errors/cli_error.rs
index 7439d1df793b0..a53fec0600b88 100644
--- a/crates/nu-protocol/src/errors/cli_error.rs
+++ b/crates/nu-protocol/src/errors/cli_error.rs
@@ -50,6 +50,10 @@ pub fn report_compile_error(working_set: &StateWorkingSet, error: &CompileError)
 
 fn report_error(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) {
     eprintln!("Error: {:?}", CliError(error, working_set));
+    let have_no_backtrace = working_set.get_env_var("NU_BACKTRACE").is_none();
+    if have_no_backtrace {
+        eprintln!("set the `NU_BACKTRACE=1` environment variable to display a backtrace.")
+    }
     // reset vt processing, aka ansi because illbehaved externals can break it
     #[cfg(windows)]
     {
diff --git a/crates/nu-protocol/src/errors/mod.rs b/crates/nu-protocol/src/errors/mod.rs
index 5f385a21ee0c6..9c729dc8f430b 100644
--- a/crates/nu-protocol/src/errors/mod.rs
+++ b/crates/nu-protocol/src/errors/mod.rs
@@ -1,3 +1,4 @@
+mod chained_error;
 pub mod cli_error;
 mod compile_error;
 mod config_error;
diff --git a/crates/nu-protocol/src/errors/shell_error/mod.rs b/crates/nu-protocol/src/errors/shell_error/mod.rs
index 6f651836ca78d..c7be1dfec5b80 100644
--- a/crates/nu-protocol/src/errors/shell_error/mod.rs
+++ b/crates/nu-protocol/src/errors/shell_error/mod.rs
@@ -1,3 +1,4 @@
+use super::chained_error::ChainedError;
 use crate::{
     ast::Operator, engine::StateWorkingSet, format_shell_error, record, ConfigError, LabeledError,
     ParseError, Span, Spanned, Type, Value,
@@ -1325,6 +1326,10 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"#
         #[label = "while running this code"]
         span: Option<Span>,
     },
+
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    ChainedError(ChainedError),
 }
 
 impl ShellError {
@@ -1373,6 +1378,16 @@ impl ShellError {
             span,
         )
     }
+
+    /// Convert self error to a [`ShellError::ChainedError`] variant.
+    pub fn into_chainned(self, span: Span) -> Self {
+        match self {
+            ShellError::ChainedError(inner) => {
+                ShellError::ChainedError(ChainedError::new_chained(inner, span))
+            }
+            other => ShellError::ChainedError(ChainedError::new(other, span)),
+        }
+    }
 }
 
 impl From<Box<dyn std::error::Error>> for ShellError {
diff --git a/crates/nu-protocol/src/pipeline/byte_stream.rs b/crates/nu-protocol/src/pipeline/byte_stream.rs
index adefbdf9f0a9a..2a7c79e1285c1 100644
--- a/crates/nu-protocol/src/pipeline/byte_stream.rs
+++ b/crates/nu-protocol/src/pipeline/byte_stream.rs
@@ -193,6 +193,7 @@ pub struct ByteStream {
     signals: Signals,
     type_: ByteStreamType,
     known_size: Option<u64>,
+    caller_spans: Vec<Span>,
 }
 
 impl ByteStream {
@@ -209,9 +210,22 @@ impl ByteStream {
             signals,
             type_,
             known_size: None,
+            caller_spans: vec![],
         }
     }
 
+    /// Push a caller [`Span`] to the bytestream, it's useful to construct a backtrace.
+    pub fn push_caller_span(&mut self, span: Span) {
+        if span != self.span {
+            self.caller_spans.push(span)
+        }
+    }
+
+    /// Get all caller [`Span`], it's useful to construct a backtrace.
+    pub fn get_caller_spans(&self) -> &Vec<Span> {
+        &self.caller_spans
+    }
+
     /// Create a [`ByteStream`] from an arbitrary reader. The type must be provided.
     pub fn read(
         reader: impl Read + Send + 'static,
diff --git a/crates/nu-protocol/src/pipeline/list_stream.rs b/crates/nu-protocol/src/pipeline/list_stream.rs
index 55ae4bfee020f..f300a443d6934 100644
--- a/crates/nu-protocol/src/pipeline/list_stream.rs
+++ b/crates/nu-protocol/src/pipeline/list_stream.rs
@@ -15,6 +15,7 @@ pub type ValueIterator = Box<dyn Iterator<Item = Value> + Send + 'static>;
 pub struct ListStream {
     stream: ValueIterator,
     span: Span,
+    caller_spans: Vec<Span>,
 }
 
 impl ListStream {
@@ -27,6 +28,7 @@ impl ListStream {
         Self {
             stream: Box::new(InterruptIter::new(iter, signals)),
             span,
+            caller_spans: vec![],
         }
     }
 
@@ -35,6 +37,18 @@ impl ListStream {
         self.span
     }
 
+    /// Push a caller [`Span`] to the bytestream, it's useful to construct a backtrace.
+    pub fn push_caller_span(&mut self, span: Span) {
+        if span != self.span {
+            self.caller_spans.push(span)
+        }
+    }
+
+    /// Get all caller [`Span`], it's useful to construct a backtrace.
+    pub fn get_caller_spans(&self) -> &Vec<Span> {
+        &self.caller_spans
+    }
+
     /// Changes the [`Span`] associated with this [`ListStream`].
     pub fn with_span(mut self, span: Span) -> Self {
         self.span = span;
@@ -94,6 +108,7 @@ impl ListStream {
         Self {
             stream: Box::new(f(self.stream)),
             span: self.span,
+            caller_spans: self.caller_spans,
         }
     }
 


Test patch:
diff --git a/crates/nu-protocol/tests/test_config.rs b/crates/nu-protocol/tests/test_config.rs
index 49ad1cdec664f..18e79635cfd78 100644
--- a/crates/nu-protocol/tests/test_config.rs
+++ b/crates/nu-protocol/tests/test_config.rs
@@ -60,7 +60,7 @@ fn fancy_default_errors() {
 
     assert_eq!(
         actual.err,
-        "Error:   \u{1b}[31m×\u{1b}[0m oh no!\n   ╭─[\u{1b}[36;1;4mline2:1:13\u{1b}[0m]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n   · \u{1b}[35;1m            ─────┬────\u{1b}[0m\n   ·                  \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n   ╰────\n\n"
+        "Error:   \u{1b}[31m×\u{1b}[0m oh no!\n   ╭─[\u{1b}[36;1;4mline2:1:13\u{1b}[0m]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n   · \u{1b}[35;1m            ─────┬────\u{1b}[0m\n   ·                  \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n   ╰────\n\nset the `NU_BACKTRACE=1` environment variable to display a backtrace.\n"
     );
 }
 
@@ -92,6 +92,7 @@ snippet line 1: force_error "my error"
     label at line 1, columns 13 to 22: here's the error
 
 
+set the `NU_BACKTRACE=1` environment variable to display a backtrace.
 "#,
     );
 }
diff --git a/tests/shell/pipeline/commands/external.rs b/tests/shell/pipeline/commands/external.rs
index 7268133ab07df..e5c6915565e5e 100644
--- a/tests/shell/pipeline/commands/external.rs
+++ b/tests/shell/pipeline/commands/external.rs
@@ -686,3 +686,34 @@ fn arg_dont_run_subcommand_if_surrounded_with_quote() {
     let actual = nu!("nu --testbin cococo '(echo aa)'");
     assert_eq!(actual.out, "(echo aa)");
 }
+
+#[test]
+fn external_error_with_backtrace() {
+    Playground::setup("external error with backtrace", |dirs, sandbox| {
+        sandbox.with_files(&[FileWithContent("tmp_env.nu", "$env.NU_BACKTRACE = 1")]);
+
+        let actual = nu!(
+            env_config: "tmp_env.nu",
+            cwd: dirs.test(),
+            r#"def a [x] { if $x == 3 { nu --testbin --fail }};def b [] {a 1; a 3; a 2}; b"#);
+        let chained_error_cnt: Vec<&str> = actual
+            .err
+            .matches("diagnostic code: chained_error")
+            .collect();
+        assert_eq!(chained_error_cnt.len(), 1);
+        assert!(actual.err.contains("non_zero_exit_code"));
+        let eval_with_input_cnt: Vec<&str> = actual.err.matches("eval_block_with_input").collect();
+        assert_eq!(eval_with_input_cnt.len(), 1);
+
+        let actual = nu!(
+            env_config: "tmp_env.nu",
+            cwd: dirs.test(),
+            r#"nu --testbin --fail"#);
+        let chained_error_cnt: Vec<&str> = actual
+            .err
+            .matches("diagnostic code: chained_error")
+            .collect();
+        // run error make directly, show no backtrace is available
+        assert_eq!(chained_error_cnt.len(), 0);
+    });
+}
diff --git a/tests/shell/pipeline/commands/internal.rs b/tests/shell/pipeline/commands/internal.rs
index 833dcaab8d81b..aa90fbf681328 100644
--- a/tests/shell/pipeline/commands/internal.rs
+++ b/tests/shell/pipeline/commands/internal.rs
@@ -1,4 +1,4 @@
-use nu_test_support::fs::Stub::FileWithContentToBeTrimmed;
+use nu_test_support::fs::Stub::{FileWithContent, FileWithContentToBeTrimmed};
 use nu_test_support::playground::Playground;
 use nu_test_support::{nu, pipeline};
 use pretty_assertions::assert_eq;
@@ -1135,3 +1135,83 @@ fn error_on_out_greater_pipe() {
         .err
         .contains("Redirecting stdout to a pipe is the same as normal piping"))
 }
+
+#[test]
+fn error_with_backtrace() {
+    Playground::setup("error with backtrace", |dirs, sandbox| {
+        sandbox.with_files(&[FileWithContent("tmp_env.nu", "$env.NU_BACKTRACE = 1")]);
+
+        let actual = nu!(
+            env_config: "tmp_env.nu",
+            cwd: dirs.test(),
+            r#"def a [x] { if $x == 3 { error make {msg: 'a custom error'}}};a 3"#);
+        let chained_error_cnt: Vec<&str> = actual
+            .err
+            .matches("diagnostic code: chained_error")
+            .collect();
+        // run `a 3`, and it raises error, so there should be 1.
+        assert_eq!(chained_error_cnt.len(), 1);
+        assert!(actual.err.contains("a custom error"));
+
+        let actual = nu!(
+            env_config: "tmp_env.nu",
+            cwd: dirs.test(),
+            r#"def a [x] { if $x == 3 { error make {msg: 'a custom error'}}};def b [] { a 1; a 3; a 2 };b"#);
+
+        let chained_error_cnt: Vec<&str> = actual
+            .err
+            .matches("diagnostic code: chained_error")
+            .collect();
+        // run `b`, it runs `a 3`, and it raises error, so there should be 2.
+        assert_eq!(chained_error_cnt.len(), 2);
+        assert!(actual.err.contains("a custom error"));
+
+        let actual = nu!(
+            env_config: "tmp_env.nu",
+            cwd: dirs.test(),
+            r#"error make {msg: 'a custom err'}"#);
+        let chained_error_cnt: Vec<&str> = actual
+            .err
+            .matches("diagnostic code: chained_error")
+            .collect();
+        // run error make directly, show no backtrace is available
+        assert_eq!(chained_error_cnt.len(), 0);
+    });
+}
+
+#[test]
+fn liststream_error_with_backtrace() {
+    Playground::setup("liststream error with backtrace", |dirs, sandbox| {
+        sandbox.with_files(&[FileWithContent("tmp_env.nu", "$env.NU_BACKTRACE = 1")]);
+
+        let actual = nu!(
+            env_config: "tmp_env.nu",
+            cwd: dirs.test(),
+            r#"def a [x] { if $x == 3 { [1] | each {error make {'msg': 'a custom error'}}}};a 3"#);
+        assert!(actual.err.contains("a custom error"));
+
+        let actual = nu!(
+            env_config: "tmp_env.nu",
+            cwd: dirs.test(),
+            r#"def a [x] { if $x == 3 { [1] | each {error make {'msg': 'a custom error'}}}};def b [] { a 1; a 3; a 2 };b"#);
+        let chained_error_cnt: Vec<&str> = actual
+            .err
+            .matches("diagnostic code: chained_error")
+            .collect();
+        assert_eq!(chained_error_cnt.len(), 1);
+        assert!(actual.err.contains("a custom error"));
+        let eval_with_input_cnt: Vec<&str> = actual.err.matches("eval_block_with_input").collect();
+        assert_eq!(eval_with_input_cnt.len(), 2);
+
+        let actual = nu!(
+            env_config: "tmp_env.nu",
+            cwd: dirs.test(),
+            r#"[1] | each { error make {msg: 'a custom err'} }"#);
+        let chained_error_cnt: Vec<&str> = actual
+            .err
+            .matches("diagnostic code: chained_error")
+            .collect();
+        // run error make directly, show no backtrace is available
+        assert_eq!(chained_error_cnt.len(), 0);
+    });
+}