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 Iterator
s, Option
s, and
newly introduced Result
.
But! Lets go in small steps
- read the first argument passed to the program
cargo run -- this_is_an_arg
npx 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 number
s 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.