How to define and work with a Rust-like result type in NuShell
Friedrich Kurz
Posted on June 21, 2024
Motivation
Rust—among other modern programming languages—has a data type Result<T, E>
in its standard library, that allows us to represent error states in a program directly in code. Using data structures to represent errors in code is a pattern known mostly from purely functional programming languages but has gained some traction in programming languages that follow a less restrictive language paradigm (like Rust).
The idea of modelling errors in code is that—following the functional credo—everything the function does should be contained in the return value. Side effects, like errors, should be avoided wherever possible.
This is a nice pattern, since it forces you to address that code may not succeed. (If you don't ignore the return value, of course. :D) Combining the use of Result
return types with Rust's pattern matching also improves code legibility, in my opinion.
Error handling in NuShell
NuShell is a very modern shell and shell language that draws some heavy inspiration from Rust and that I really enjoy writing small programs and glue code in (especially for CI/CD pipelines).
In contrast to Rust, NuShell has a try
/catch
control structure to capture and deal with errors. There is no result type in the standard library at the time of writing.
NuShell's try
/catch
, moreover, has the major downside, that you cannot react to specifics of an error, since the catch
block doesn't receive any parameter like an exception object, that would allow us introspection on what went wrong.
So what can we do? Well, we may just define a Result type ourselves and use it. Since NuShell also has pattern matching using the match
keyword, we can write some pretty readable code with it.
Consider, for example, malformed URLs when using the http
command (in this case the protocol is missing):
nu> http get --full www.google.com
Error: nu::shell::unsupported_input
× Unsupported input
╭─[entry #64:1:1]
1 │ http get --full www.google.com
· ────┬─── ───────┬──────
· │ ╰── value: '"www.google.com"'
· ╰── Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com
The code above will crash. As mentioned before, we could use try
/catch
. But the problem remains, how do we enable the calling code to react to errors?
use std log
try {
return http get --full www.google.com # Missing protocol
} catch {
log error "GET \"www.google.com\" failed."
# Now what?
}
Using a result type (and some convenience conversion functions into ok
and into error
), we can write a safe http get function as follows:
def safe_get [url: string] {
try {
let response = http get --full $url
$response | into ok
} catch {
{url: $url} | into error
}
}
We could use it in our code like this:
nu> match (safe_get "https://www.google.com") {
{ok: $response} => { print $"request succeeded: ($response.status)" },
{error: $_} => { print "request failed" }
}
request succeeded: 200
And for the failure case:
match (safe_get "www.google.com") {
{ok: $response} => { print $"request succeeded: ($response.status)" },
{error: $_} => { print "request failed" }
}
request failed
Now the calling code can react to failure by disambiguating the return values and processing the attached data.
Addendum
Here are the helper functions into ok
and into error
for completeness sake.
export def "into ok" [value?: any] {
let v = if $value == null { $in } else { $value }
{ok: $v}
}
export def "into error" [cause?: any] {
let c = if $cause == null { $in } else { $cause }
{error: $c}
}
Posted on June 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.