47
loading...
This website collects cookies to deliver better user experience
try...catch
and null
are just the tip of the iceberg!Result
enum and pattern matching can do for you, and how this extends to handling null
values.catch
dance all throughout your JavaScript codebase. Take this scenario:// Scenario 1: catching a dangerous database call
app.get('/user', async function (req, res) {
try {
const user = await dangerousDatabaseCall(req.userId)
res.send(user)
} catch(e) {
// couldn't find the user! Time to tell the client
// it was a bad request
res.status(400)
}
})
400
when it doesn’t.try...catch
here? Well, with a name like dangerousDatabaseCall
and some intuition about databases, we know it’ll probably throw an exception when something goes wrong.// Scenario 2: forgetting to catch a dangerous file reading
app.get('/applySepiaFilter', async function (req, res) {
const image = await readFile("/assets/" + req.pathToImageAsset)
const imageWithSepiaFilter = applySepiaFilter(image)
res.send(imageWithSepiaFilter)
})
applySepiaFilter
, we want to read the requested file out of our server’s /assets
and apply that color filter.try...catch
around this! So, whenever we request some file that doesn’t exist, we’ll receive a nasty internal server error. This would ideally be a 400
“bad request” status. 😕try...catch
…” Understandable! Some Node.js programmers may immediately recognize that readFile
throws exceptions. =throw
s, the caller must remember to handle that exception. And no, your fancy ESlint setup won’t help you here! This can lead to what I'll call try...catch
anxiety: wrapping everything in a try
block in case something goes wrong. Or worse, you’ll forget to catch
an exception entirely, leading to show-stopping failures like our uncaught readFile
calltry...catch
wrappers around multiple points of failure. For example, what if our readFile
explosion should return one status code, and an applySepiaFilter
failure should return another? Do we have multiple try...catch
blocks? What if we need to look at the exception’s name
field (which may be unreliable browser-side)?Result
enum.try...catch
block. Heck, they don’t even have “exceptions” as we’ve come to know them.meal
from our Korean street food menu, and we want to serve them a different meal
depending on the orderNumber
they chose.let meal = null
switch(orderNumber) {
case 1:
meal = "Bulgogi"
break
case 2:
meal = "Bibimbap"
break
default:
meal = "Kimchi Jjigae"
break
}
return meal
switch
statement): Our meal
needs to start out as null
and needs to use let
for reassignment in our switch
cases. If only switch
could actually return a value like this…// Note: this is not real JavaScript!
const meal = switch(orderNumber) {
case 1: "Bulgogi"
case 2: "Bibimbap"
default: "Kimchi Jjigae"
}
let meal = match order_number {
1 => "Bulgogi"
2 => "Bibimbap"
_ => "Kimchi Jjigae"
}
match
is considered an expression that can:meal
)const meal = orderNumber === 1 ? "Bulgogi" : "Something else"
if
statement:let meal = if order_number == 1 { "Bulgogi" } else { "Something else" }
return
. The last line of a Rust expression is always the return value. 🙃applySepiaFilter
endpoint from earlier. I’ll use the same req
and res
helpers for clarity:use std::fs::read_to_string;
// first, read the requested file to a string
match read_to_string("/assets/" + req.path_to_image_asset) {
// if the image came back ay-OK...
Ok(raw_image) => {
// apply the filter to that raw_image...
let sepia_image = apply_sepia_filter(raw_image)
// and send the result.
res.send(sepia_image)
}
// otherwise, return a status of 400
Err(_) => res.status(400)
}
Ok
and Err
wrappers? Let’s compare the return type for Rust’s read_to_string
to Node’s readFile
:readFile
returns a string
you can immediately work withread_to_string
does not return a string, but instead, returns a Result
type wrapping around a string. The full return type looks something like this: Result<std::string::String, std::io::Error>
. In other words, this function returns a result that’s either a string or an I/O error (the sort of error you get from reading and writing files)read_to_string
until we “unwrap” it (i.e., figure out whether it’s a string or an error). Here’s what happens if we try to treat a Result
as if it’s a string already:let image = read_to_string("/assets/" + req.path_to_image_asset)
// ex. try to get the length of our image string
let length = image.len()
// 🚨 Error: no method named `len` found for enum
// `std::result::Result<std::string::String, std::io::Error>`
unwrap()
function yourself:let raw_image = read_to_string("/assets/" + req.path_to_image_asset).unwrap()
unwrap
and read_to_string
returns some sort of error, the whole program will crash from what’s called a panic. And remember, Rust doesn’t have a try...catch
, so this could be a pretty nasty issue.match read_to_string("/assets/" + req.path_to_image_asset) {
// check whether our result is "Ok," a subtype of Result that
// contains a value of type "string"
Result::Ok(raw_image) => {
// here, we can access the string inside that wrapper!
// this means we're safe to pass that raw_image to our filter fn...
let sepia_image = apply_sepia_filter(raw_image)
// and send the result
res.send(sepia_image)
}
// otherwise, check whether our result is an "Err," another subtype
// that wraps an I/O error.
Result::Err(_) => res.status(400)
}
_
inside that Err
at the end. This is the Rust-y way of saying, “We don’t care about this value,” because we’re always returning a status of 400
. If we did care about that error object, we could grab it similarly to our raw_image
and even do another layer of pattern matching by exception type.Result
? It may seem annoying at first glance, but they’re really annoying by design because:unwrap()
try...catch
anxiety, and no more janky type checking 👍null
(or undefined
) when we have some sort of special or default case to consider. We may throw out a null
when some conversion fails, an object or array element doesn’t exist, etc.null
return values in JS because throw
ing an exception feels unsafe or extreme. What we want is a way to raise an exception, but without the hassle of an error type or error message, and hoping the caller uses a try...catch
.null
from the language and introduced the Option
wrapper. ✨get_waiter_comment
function that gives the customer a compliment depending on the tip they leave. We may use something like this:fn get_waiter_comment(tip_percentage: u32) -> Option<String> {
if tip_percentage <= 20 {
None
} else {
Some("That's one generous tip!".to_string())
}
}
""
when we don’t want a compliment. But by using Option
(much like using a null
), it’s easier to figure out whether we have a compliment to display or not. Check out how readable this match
statement can be:match get_waiter_comment(tip) {
Some(comment) => tell_customer(comment)
None => walk_away_from_table()
}
Result
and Option
is blurry. We could easily refactor the previous example to this:fn get_waiter_comment(tip_percentage: u32) -> Result<String> {
if tip_percentage <= 20 {
Err(SOME_ERROR_TYPE)
} else {
Result("That's one generous tip!".to_string())
}
}
...
match get_waiter_comment(tip) {
Ok(comment) => tell_customer(comment)
Err(_) => walk_away_from_table()
}
Err
case, which can be a hassle because the callee needs to come up with an error type /
message to use, and the caller needs to check whether the error message is actually worth reading and matching on.get_waiter_comment
function. This is why I’d usually reach for an Option
until I have a good reason to switch to the Result
type. Still, the decision’s up to you!exception
and null
handling is a huge win for type safety. Armed with the concepts of expressions, pattern matching, and wrapper types, I hope you’re ready to safely handle errors throughout your application!