-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Feature Request: Throw-by-default Error Handling #10633
Description
Related problem
I use Nushell's scripting language mostly to handle calling executables and parsing their output, or performing some set of bulk operations. But as I build more complicated scripts with Nushell, its default handling of errors from executables is starting to feel like a big footgun.
Many Nushell built-ins already handle errors like I'd expect, something like "throw" in other languages. Execution is stopped, and the user gets a detailed error:
> open asdfasdf | split row " " | print; print done
Error: nu::shell::directory_not_found
× Directory not found
╭─[entry #63:1:1]
1 │ open asdfasdf | split row " " | print; print done
· ────┬───
· ╰── directory not found
╰────
But not everything is a built-in. If I need to reach for an executable to perform some function, the behavior changes:
cat asdfasdf | split row " " | print; print done
cat: asdfasdf: No such file or directory
╭───┬──╮
│ 0 │ │
╰───┴──╯
done
Now the error is swallowed. cat is nice enough to print an error to stderr, but it didn't stop execution. Nushell confidently returned the wrong value to me, and carried on with its execution.
It might not seem so bad in this toy example, where I can see the error message on screen. But other cases might have returned enough results to push the error message off of the screen.
If the usage of cat is buried in N levels of function calls, the distance between the place where the real error occurred (no such file) and where Nushell might eventually error (say, referencing a column/value that should exist) may be large, and difficult to debug.
Describe the solution you'd like
- Non-zero exit codes should "throw" (
error make) everywhere by default.
I'd expect cat asdfasdf to fail the same way that this fails today:
> do -c { cat asdfasdf }
cat: asdfasdf: No such file or directory
Error: nu::shell::external_command
× External command failed
╭─[entry #71:1:1]
1 │ do -c { cat asdfasdf }
· ─┬─
· ╰── External command failed
╰────
help:
- The user should be able to opt out of this behavior if they want. (
do -i/-p,try/catch, or something.) But this behavior should be local and not inherited.
Ex: As an author of a nushell library, I should be able to write a function like:
def doThing [] {
print "Starting thing…"
extern-tool foo
extern-tool bar --baz # Shouldn't be run if the previous line failed. Might do bad things in that case.
print "Success!"
}… and rely on the "throw by default" behavior of NuShell to exit my function with an error showing that extern-tool failed and which call failed, with no extra effort from me.
And an external user shouldn't be able to disable the default error handling which my function relies on. I call this out because users can seem to do something similar with do -c and subexpressions currently:
~> def ex1 [] {
::: print start
::: cat asdfasdf
::: print done
::: }
~> ex1
start
cat: asdfasdf: No such file or directory
~> do -c { ex1 }
start
cat: asdfasdf: No such file or directory
Error: nu::shell::external_command
× External command failed
╭─[entry #27:2:1]
2 │ print start
3 │ cat asdfasdf
· ─┬─
· ╰── External command failed
4 │ print done
╰────
help:
~> (ex1)
start
cat: asdfasdf: No such file or directory
done
- There could be well-defined exceptions to throw-by-default.
For example, if you pipe directly to | complete, you explicitly have stderr and the exit code. The assumed intention is that you're going to check those. So you could skip the error make in this case. (If implementing an exception in pipes is a pain, maybe make a command like exec that captures the outputs? ex: capture cat asdfasdf)
Simple commands run directly in the REPL (i.e. not inside of a function or block, or chain of commands) could forego error make under the assumption that they'll report their own errors. (ex: the git log example.)
Describe alternatives you've considered
Just wrap your calls to executables in do -c { … }
"Just don't shoot yourself with the foot-gun every time you and anyone else uses it." 😬
But also: It's not always easy to know what is an executable vs. what's a built-in. You might think that git is an executable, so git branch needs to be wrapped in a do -c { … }, but that's actually unnecessary in my shell because I have a custom git branch command.
IMO shell languages are nice when they blur the lines between built-in and binary commands. I'd love it if errors just operated the same way in both cases.
Additional context and details
I originally had much of this conversation in #10624 but I'm moving the discussion here to let that ticket focus on the existing bug, and this issue can be the request for a change in the fundamental behavior.