To throw or not to throw, that is the question
Another place where typescript and rust have fundamentally different design decisions.
Ask yourself the following questions
- what function throws an error?
- who handles the error if thrown?
- what can be throw? (look at the promise reject definition (reason: any))
with javascript you learn by trial.
How Rust Handles Errors
Errors are values
This means that there is no throwing. You get a value that is either a value or an error.
Notice this is very similar to Option.
- Option is value or undefined
- Result is value or error value
if err != nil
yes, the golang meme of error handling doesn't exist in rust. Rust handles errors better than go.
I am going to make some assumptions
You don't need me to show you another example of how to handle enums because that's all we have been doing.
The definition of a result
enum Result<V, E> {
Ok(V),
Err(E),
}
Also, rust has Ok and Err as first class citizens
if let Ok(value) = a_function_that_can_error() {
// something with the value
}
match a_function_that_can_error() {
Ok(value) => println!("oh yeah, value! {}", value);
Err(e) => eprintln!("ohh no... {}", e);
}
// you don't care about the error
_ = a_function_that_can_error();
// yolo
let foo = a_function_that_can_error().unwrap();
// respectful yolo
let foo = a_function_that_can_error().expect("should never fail");
// defaults
let foo = a_function_that_can_error().unwrap_or(0);
// convert to option
// Ok(V) => Some(V)
// Err(E) => None
// bai felicia
let foo = a_function_that_can_error().ok();
let foo = a_function_that_can_error()
.map(|value| value + 1);
let foo = a_function_that_can_error()
.and_then(|value| another_possible_error(value))
.and_then(|value| again(value));
// If your function returns an error, you can do this!
let foo = a_function_that_can_error()?;
Side Note
there are two crates (rust package) that work very well with errors
- thiserror - great for creating your own errors. should be used in libraries
- anyhow - great for applications.
We will use anyhow shortly
Another small exercise!
This is going to combine all of our knowledge of Iterators, Options, and
newly introduced Result.
But! Lets go in small steps
- read the first argument passed to the program
cargo run -- this_is_an_argnpx ts-node file this_is_an_arg
- the first argument is a name and path to the file to read
- print out each line of the file
process.argv[2] <--- first arg to program
Now we have already done some of this, so this should become a bit easier, lets start with TypeScript.
file you should use, save as proj/numbers
1
5
9
33
Complete Code
import fs from "fs";
fs.readFileSync(process.argv[2])
.toString()
.split("\n")
.forEach((line) => console.log(line));
Ok... now try this
npx ts-node src/index.ts filethatdoesntexist
what happens? why?
ok... again
npx ts-node src/index.ts
?? WHY YOU DO THIS TO ME
Rust
Ok lets try the same thing in rust. It will be a bit more involved.
I'll give you some hints.
std::env::args().nth(1) // <--- gets the first argument
// passed to the program
if you forgot
std::fs::read_to_string(...) // reads a file to string
I'll give you a second, then i'll do it
Complete Code
fn main() {
let arg = std::env::args().nth(1)
.expect("please provide a file name as an argument");
std::fs::read_to_string(arg)
.expect("unable to read the file provided")
.lines()
.for_each(|line| println!("{}", line));
}
[Discussion]: Compare TypeScript w/ Rust
what makes rust better or worse in this example?
Lets add more requirements
lets only print out lines that are numbers and lines that are not, lets print
out Line not a number
First, TypeScript, i'll give you a moment
Complete Code
import fs from "fs";
fs.readFileSync(process.argv[2])
.toString()
.split("\n")
.forEach((line) => {
const v = parseInt(line);
if (isNaN(v)) {
console.log("Line not a number");
} else {
console.log(v);
}
});
Now how do we do this in rust?
one piece of knowledge, parse is needed.
A &str has parse function which allows for any Type implementing
FromStr to be parsed from a string. Now this sounds like a bunch of
non-sense, don't worry, we will go through this more deeply soon.
// -------v
let foo: usize = "5".parse();
// ---------------------v
let foo = "5".parse::<usize>();
// ------------v
fn mult() -> usize {
return "5".parse().unwrap_or(0);
}
I'll give you a moment to try it out, then i'll do it (think pattern matching)
Complete Code
fn main() {
let arg = std::env::args().nth(1)
.expect("please provide a file name as an argument");
std::fs::read_to_string(arg)
.expect("unable to read the file provided")
.lines()
.for_each(|line| {
if let Ok(value) = line.parse::<usize>() {
println!("{}", value);
} else {
println!("Line not a number");
}
});
}
[Discussion]: Which one was easier to get right?
Try to think through the problem as if you knew Rust as well as TypeScript
A Case for rust
In the simplest sense, you always know where your errors happen, you always know when undefineds can happen
- Result saves you from errors you should be able to prevent
- Option saves you from
undefined is not a function - Rust doesn't save you from bad logic, we are all bad programmers, sowwy
Questions?
Get them out of the way now, even if its not Result based.
Remember:
- If you don't understand something, this is a great time to understand it better
- If you don't understand something, guarantee the person next to you is struggling with the same thing
- If you don't ask, who will?
The next section is going to be much more rust focused.