Rust for TypeScript Developers

Results & Error Handling Exercise



Rust for TypeScript Developers

Check out a free preview of the full Rust for TypeScript Developers course

The "Results & Error Handling Exercise" Lesson is part of the full, Rust for TypeScript Developers course featured in this preview video. Here's what you'd learn in this lesson:

Students are instructed to write a program that reads an argument from the command line that specifies the name and path of a file. The file should be loaded, and each line should be printed.


Transcript from the "Results & Error Handling Exercise" Lesson

>> All right, so let's do another small exercise. This is gonna take all of our knowledge now, iterators, options, results, and we're gonna do this. So the first thing we're gonna do is we're gonna actually create a TypeScript program that reads the first argument passed into it, reads that as a file.

It's gonna print out each line in that file. Sounds good? All right, should be pretty easy, and this is the file I want you to create, this right here, 1, 5, 9, 33. You can call it numbers, you can call whatever you want, I'll probably just call mine numbers.

So I'm just gonna copy and paste this, 1, 5, 9, 33. I'm gonna create that, numbers, paste it in, no extra new line, boom, looks so good. All right, so let's go back here, let's see the instructions that I have set forth. So read the first argument passed into the program.

For those that don't know how to do that, that's process.agrv, position [2], first argument into the program. Next, that first argument, by the way, should be the name and path to the file we want to read. Then we're gonna read the file and print out each line. And we're gonna do it line by line.

So first, we gotta import fs from "fs", stands for file system. Next, we're gonna wanna get that file name, right? So process, my goodness, process.argv[2]. It's argv[2] because it's node, file, argument. Whereas with Rust, it's executable argument. So you don't get the second one, you get the first one, a little bit different, but anyways.

So we got the file name. Next, let's go fs.readFileSync, and pass in fileName and then do what we do best, which, of course, is toString, bam. split new line, forEach, console.log(line), look at that. Whoopsie daisies, we gotta probably make this, look at that. Cuz we have our TypeScript, so great.

So this is one thing we did enable all those extra options within our TypeScript TsConfig, so if you didn't do that, you wouldn't get this error right now. But since I enabled even undefined checks on array checks, we get this beautiful little thing right here. So that's fantastic, right?

So we can do something like, if not (!fileName). Actually, hold on, it kinda ruins the magic. Hold on, I'm gonna go back down here. I had a whole great point here, so I'm just gonna go like this. Just put that back to here, isn't that how most people do anyways?

There we go, look at how beautiful that is. There's no errors, we're using TypeScript, all right, okay. Do not pay attention to the man behind the mirror right now. So I'm gonna go like this, npx ts-node, let's call it numbers, right? And, But whoopsies, we wanna go src/index.ts and then pass in numbers.

There we go, we got 1, 5, 9, and 33. Okay, fantastic. So hopefully, everyone arrived to the same conclusion. And so let's do the exact same thing, but this time, or before we do that, we already kinda talked about this earlier, look at what I could pass into the program.

Did everybody properly handle the error for readFileSync? I mean, I didn't. If we didn't have that one option on that told us it could be undefined and we happen to have the most strict TypeScript settings, did you handle the fact that it could be undefined? I could just pass in nothing, right?

That's a problem, it's kind of a big problem here, which is that, we never got alerted to any of the things we are doing wrong with TypeScript. So let's do the exact same thing in Rust, except this time, this is just how you read the first argument passed into the program, and this is how you read a file just to a string.

We're not gonna do anything efficient, we don't care about efficiency right now, we're just doing it the easy way. So I'll program this with you, we'll slow it down a little bit. All right, so let's remove that, and so we can go like this. Let arg = std::env::args().

Now, args, if you look at it, it's actually just an iterator, you get to iterate over all the arguments passed into it. So I can call something called nth, which takes the nth item into the iterator. So this one just happens to be giving me the first item or technically the second item in the iterator.

And let's see what the type is. So if we go over here and hover over arg, does everyone have this, Option<String>? So right away, it's forcing you to handle this, because Rust already knows that you may not have two arguments being passed in. You may only have one, the name of the program.

So therefore, we can use something called expect. So I'll go here and I'll go like this, expect("the file name to be passed in"), right? Because our program should crash with a reasonable message if it exists. So now, our arg is the right type, and we have a nice crash message, so that's pretty cool.

All right, I'm liking it right now. So let's do file now, std::fs::read_to_string. And now, we can just pass in our arg. Really, we should probably call that file_name, maybe a little bit more descriptive here, don't you agree? And so now we can read it to string. All right, so what's the type on file?

Can anyone guess? Does anyone know? Is anyone keeping up with me?
>> Vector of strings?
>> It's result of string?
>> Boom, result of string, because we're reading it just to a string. Remember, a vector and the string, they're kind of like the same type when you really think about it.

You have to really think about it for a little bit. Both are dynamically grown things that live on the heap, and which you can grow and add more stuff to. It's just a string, has UTF-8 slots put in, whereas a vector is any type. So they're almost like the same thing when you really think about it.

You can actually convert somewhat between them. We have this nice result string, so we need to do something with this. Well, the reality is, if there's an error, we should probably just crash the program, right? So expect ("unable to read the file to string"). Look at that. Oh-oh, my keyboard just died on one side, went too fast, got a flat tire.

All right, look at that. Who here builds simple programs and actually handles all your errors? I expect no hands up. We all know we take shortcuts. Well, guess what? You actually literally can't take a shortcut in Rust, you can't do that. We've just handled all of the possible error conditions that can go wrong in our program already, it's pretty cool.

Hopefully you start to see the value of the basic proposition of Rust. So now, we can just do file.lines() for_each(|line|. We've done this a bunch of times at this point, println!("{}, |line|)), all right? Look at that, I have just printed out every single line right there. Pretty straightforward, so again, lines is an iterator, it's not greedy.

It's a pull-based, so I have to pull out each line one at a time. And then for_each gets called each time a line comes out. All right, so let's go back here and let's just go cargo run. Of course, cargo run is how you run a Rust program.

And so when we run it, look at that, we got a panic. And what's our panic say? Well, we can see right here, the file name to be passed in. I didn't give a very good error, but nonetheless, we kinda know what went wrong here, I forgot to pass in the file name.

So with cargo, you have to do a little dash dash, and then pass in your file name, I'll go numbers. The dash dash you've seen in other programs, usually that just means pass these arguments into the program that cargo is going to run. So there we go, 1, 5, 9, 33.

Well, fantastic, so if I put in a fake file, no such file or directory, unable to read string to file. So we not only see the underlying error, which is unable to find the file, we see our nice pretty string error, unable to read the file to string.

All right, fantastic, we have all the right stuff, we could fix this problem, we know what went wrong. So there we go. So discussion, real talk, what makes Rust better or worse in this situation? What do you like or not like about it? Don't make me call on someone.

>> I like that you discover errors during development rather than runtime.
>> Correct, so if you couldn't hear, I don't know how the microphone situation is, you discover errors during development as opposed during runtime. I think that is my biggest favorite takeaway, which is you just always know that it's way easier to handle something at compile time than production.

It's way easier to handle something at compile time than CI. It's way easier to handle it at pre-push. Each one, it just gets easier and easier and easier, and I actually just scrolled all the way down. And so, let's see, where is discussion? There we go. I think that's the first one, okay, there we go.

So yeah, that's kind of what I like. What I like about TypeScript is that I can just move super fast. But if something goes wrong, the slowdown is significantly more. You have to kinda figure out what is going wrong, whereas you're just gonna see it sooner. So you may slow down a little bit in the beginning for a speed up in the end, versus a speed up in the beginning for a slowdown in the end.

So it's kind of where do you wanna spend your slow? That's kind of how I tend to think about things,, is that most programing languages, most activities we do can be all done in about the same amount of time, it's just where do you wanna spend the headache?

For me, I'd rather spend it pre-compile versus broad. Personal opinion, maybe I'm being a little bit dramatic here, a little dramatic, but you get the idea. So let's add one more requirement. Let's only print out lines that are numbers and lines that are not numbers. Let's print out line, not a number.

So first, let's do it in TypeScript, this should be pretty straightforward. I'm curious how people do it, cuz there's a couple of different ways you can do this. So personally, how I would do it is I just handle it within the forEach statement itself instead of doing a filter or a map, because it's just, I mean, just adds more complexity in general.

So I'll do something along the lines of this. Let's see, we'll go like this. Let print = parseInt(line) if print isNaN,. Did you remember to do a NaN? Who remembered to do the NaN? If it is NaN, I want you to console.log, or I guess we don't have to do that, we can just go const, there we go.

We're gonna console.log. What did I say to log? Line not a number. Line not a number, right, maybe I didn't even say that. else, whoopsie-daisies, we'll do print. There we go. Just kinda handle it there, I don't really see a point of doing a map or anything like that.

No need to read through an array just to read through an array again. So good enough right here. And so when I execute it, what happens when I execute this? Could anyone guess what I'm gonna print out? Remember, the file is a list of numbers.
>> Yeah, we shouldn't get the same stuff.

>> Yeah, you're wrong, so the fun part is that what's node for whatever reason, cuz how split works, the last remaining new line, which is at the end of every single file, actually gets its own empty string. So you may not realize that was gonna happen, a lot of people kinda get caught off guard with that last little one right there.

It's kinda funny how it happens, but hey, most people didn't see that one coming. All right, I didn't see it coming, it actually took me a while to figure out what the heck was going on there. So if we do the exact same thing with Rust, we can do this, but here, I think I put a nice little hey.

So here's a little piece of Rust knowledge, every string has a method called parse on it. What parse calls depends on the type it's being assigned to, and there's a trait you can implement or an interface you can implement that allows you to override this method for your type on a string, super cool.

But that means we have to have it specified somewhere. So either the variable needs to say, hey, I'm a usize, parse, or it needs to be specified here, or it needs to be specified in the return statement. We need somewhere to tell us what type are we parsing to, cuz then the compiler can figure out what code to execute for us to do the parsing.

Pretty straightforward, pretty nice. And one really nice part is that every single type, there's only one parse method, parse. I don't have to know about parseInt. I don't have to know about parseFloat. I don't have to create my own custom parse for every single string potential out there.

It's just, there's a singular method. So let's do that now. So I'm gonna go like this, I'm gonna go, let's see, we'll call it print again, = line.parse, and I'm gonna call it a usize. All right, so what's going on here, what's wrong here? Oh-oh, If you try to parse something, it returns a result.

Something goes wrong, right, because if you try to parse an int, it may not be successful. And so instead of just letting NaN happen by accident, it tells you upfront, this thing might not happen, there is no NaN. So let's do a little pattern match, how does that sound?

So I can go, if let Some(value) = line.parse, and I'll have to define this somewhere, so I'll use what is referred to as the turbofish operator. There we go, we have this, and what do we got here? Oop, that's a value, that's an option, I have to do Ok.

Ok is for errors, sum is for options. So there we go. I have just lifted out that value, I have this, and now I can println the value, else, whoopsy-daisy, I can bring this in and say, ("Not a number", or I think it is "Line not a number".

And of course, it's a macro, there we go. Look at that, gave us something, we handled the error up front, we did the right thing, we had to do the right thing. We didn't have a choice, there's no accidental forgetting about numbers becoming that favorite type of number, undefined.

And if I cargo run, You get all the right things. You also don't get that last new line, the empty string thing, cuz it just doesn't happen. But nonetheless, we got numbers, we got numbers, we only printed numbers. If I were to go in here and jump into numbers and just put in here foo and rerun it, we'll see that right in the middle, everything happened, awesome.

And the best part is we're still just using iterators and enums. We haven't even actually changed our types yet, we're still just using all the same thing. And so it just makes it a little bit easier to always have the same mental model. You can use the same match statements, you can use the same if statements, you can use the same pattern matching techniques that you like the best, they're all just right here.

All right, so here's my basic case for Rust. In the simplest sense, you always know where errors happen, you always know when undefined happens, there is just no getting around that. Rust saves you from errors, Rust saves you from options, but the one thing Rust doesn't save you from is being a bad programmer.

I'm sorry, we're all bad programmers. You can still leak memory in Rust, just put stuff into a map until your program explodes. It's not hard to actually do the wrong thing, that's just bad logic. But at least, we can be saved from things that we should never have to be worried about to begin with.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now