Production-Grade TypeScript

Production-Grade TypeScript Error Handling with Unknown

Learning Paths:
Topics:
Check out a free preview of the full Production-Grade TypeScript course:
The "Error Handling with Unknown" Lesson is part of the full, Production-Grade TypeScript course featured in this preview video. Here's what you'd learn in this lesson:

Mike uses the "unknown" type on an error property in a catch() clause. If an error is thrown as a String instead of an Error object, the catch() class can still handle the error and determine its type.

Get Unlimited Access Now

Transcript from the "Error Handling with Unknown" Lesson

[00:00:00]
>> So speaking of errors, catching, that involves errors, right? So I could do here, somethingRisky and we'll define that up here. It might throw, so you're used to doing something like this. Now, here's the challenge, especially if somethingRisky it's calling into other functions, right? They might throw an error.

[00:00:31] They might also throw a string. I don't like when strings are thrown because in some JavaScript runtimes, you won't get a stack trace with that string. And an error can have some metadata on it like a stack, and you can use that to render it nicely somewhere. So, but you could anything here, right?

[00:00:53] Like the whole point of catching is something unplanned happened. It's an exception, it's exceptional. We didn't think this would happen, right? So you shouldn't be using try catch as like a control flow mechanism, it should be for exceptional things. Now, here an error is of type any. So I could go in here and I could say, Right, if this is an instance of an error object like capital E error, there should be a property on it called stack.

[00:01:33] And TypeScript is not yelling at me here. So this could work fine and then somewhere deep in what somethingRisky calls, someone could throw a string. Stack is not there anymore and now in the code that is supposed to gracefully handle an unexpected failure, I'm gonna blow up and another unexpected error is going to happen, so this is bad.

[00:01:54] TypeScript 4.0 allows us to do this and I would advise you always do this. You can type this argument as an unknown, so like any, unknown is a top type, it can hold any value. But the difference between unknown and any, is that with unknown it's on us to perform some sort of check on it, right, to narrow that down and to validate that it is of a specific type before we can go ahead and reach into it and use it.

[00:02:31] So here we could do something like this. If (err instanceof Error), we can go ahead and do this, which is fine. Right in here it's gonna be this error. Then we could just like log the whole thing out. I mean, I would advise you don't just like console.log your errors here, but now we can handle like.

[00:02:57] It encourages us to do the responsible thing here and at runtime, we have to make sure this is what we think it is, and then handle the case where it's something other than what we thought it would be. This is really nice and I think it will help set up some nice guardrails for many teams for proper error handling, because mistakes in this area of your code can lead to loss of information that makes troubleshooting much much harder.

[00:03:25] This is where if you've got Google Analytics or Bugsnag, something like that and you just get garbage data or you just see like people have crashed, but we need more information like we're not getting a stack, we don't know what's happening. A lot of those kinds of problems can be tied back to making overly optimistic assumptions about what was thrown.

[00:03:49] And this forces you to sort of deal with validating that this is what we expected it to be. And then even handling the case where it's something strange, nice little addition there. While we're talking about this here, which is a type guard, something was added in TypeScript 3.7, which I feel like is also a big deal, and it is a way to type assertions more effectively.

[00:04:20] So, before TypeScript 3.7, We could do something like this. And let's move this thing that we just checked, we can return it here. And we can call this, and it'll be equivalent. So we're just simply, we're making this little user defined type guard, we've replaced our explicit check with a call into this function.

[00:04:53] But it behaves exactly the same way, meaning if that condition is evaluated to be true. Because we've described the return type of this function as not just a Boolean but an indication of whether the value is of type error. It's kind of this little place where compile time type checking and runtime code execution align, it gives us the ability to implement this check.

[00:05:23] And it can be used to narrow down this type from an unknown to an error. So this was our only tool in the toolbox and as of TypeScript 3.7, we can do this. Instead of just saying, I'm returning true or false. Gotta get my parens. We're not proud of all the code rewrite, but this is a good way to demonstrate this, I don't advise that you use this specific code in your app.

[00:06:19] But what I do want you to see is instead of relying on an if/else, combined with this type guard that returns a Boolean, now we're defining a different kind of type guard where the thing that the compiler interprets, right? Which is these instructions here, it's not like whatever I return should be the signal that we listen to, it's whether I return or throw that should be observed, right?

[00:06:51] So if I throw, I'm not an instanceof Error, if I return, you can assume that we have found an error, which is why we see this kind of narrowing down here, just as if I had a conditional, right? Now I might have this kind of code here in my test suite, that's where assertions kind of belong to be honest and, yeah, I might put this kind of code in my test suite.

[00:07:19] I might have something like, this would probably be a negative test case. Because obviously, you'll either throw here or you'll log this out, like depending on what error turns out to be. But the ability to type assertions properly is really important, and the fact that the narrowing actually works here without a conditional.

[00:07:42] The biggest benefit this adds to your test suite is you no longer have to cast things or use that the shibang like the, forget what it's called, the non-null assertion operator, right, where it's kind of like that optional chaining, but it's an exclamation mark. So you no longer have to do that, right?

[00:08:08] The assertions that you would have written in your tests, they can now be typed, such that the code that you wanted to write is also the code that makes the compiler happy. And this was not the case until this was added. Before I move on, any questions about assertions and errors?

[00:08:29] Right, so just to recap, we got PS expect error, we have the ability to treat arguments passed to a catch block as unknown. Which we have, that's this here, right? And then we have the ability to properly type assertion functions. Those have all been added since we recorded the last course.

[00:08:51] So the question is, with this specific code I wrote, would this be a good idea? Wouldn't we simply just rethrow from this assertIsError if things blew up. Yes, we would. And honestly, I would probably be okay with this in my test suite, because I like test runners and assertion libraries that treat a throne error as a failed assertion.

[00:09:13] So, if this is in my test suite and I get a throw on line 53, I know something's wrong. That is a test fail. I've caught a problem. Does that make sense? So, like that's exactly what I want in my test suite. But not in my app code for sure.

[00:09:33] If this were in app code, we would be trading one error for another. So this is more test suite kind of code, where I just like, I'm not swallowing any errors here for sure. If it's the error other than the one I expected, I get a different error, that's nice.

[00:09:51] I've got like some extra signal there.