Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Handling errors in TypeScript the right way

Posted on Sep 5 • Originally published at blog.avansai.com Error handling is one of the key areas in software engineering. When done correctly, it can save you hours of debugging and troubleshooting. I’ve identified three main challenges related to error handling:Let’s delve deeper into each of them to better understand the challenges they present.In JavaScript, the most common way to handle errors is similar to that in most programming languages:You’re going to end up seeing an object that looks like this:This seems straightforward, so what about TypeScript? One of the first things I noticed is that when you use a try/catch block and check the type of error, you’ll find that its type is unknown.For those new to TypeScript, this can be perceived as an annoyance. A common workaround for this issue is to simply cast the error, as shown below:This approach likely works for 99.9% of caught errors. But why does TypeScript make error typing seemingly cumbersome? The reason is that it’s genuinely impossible to infer the type of “error” because a try/catch block doesn’t catch only errors; it catches anything that’s thrown. In JavaScript (and TypeScript), you can throw almost anything, as demonstrated below:Executing this code will result in a new error being thrown within the “catch” block, which negates the purpose of using a try/catch in the first place:Uncaught TypeError: Cannot read properties of undefined (reading 'message') at :4:20The issue arises because the message property doesn’t exist on undefined, leading to a TypeError within the catch block. In JavaScript, only two values can cause this issue: undefined and null.Now, one might wonder about the likelihood of someone throwing undefined or null. While it’s probably rare, if it does occur, it can introduce unexpected behaviors in your code. Furthermore, considering the plethora of third-party packages typically used in a TypeScript project, it wouldn’t be surprising if one of them inadvertently throws an incorrect value.Is this the only reason TypeScript sets the type of throwables to unknown? At first glance, it might seem like a rare edge case, and casting could appear as a reasonable solution. However, there’s more to it. While undefined and null are the most disruptive cases, as they can crash your application, other values can also be thrown. For example:The primary distinction here is that, rather than throwing a TypeError, this will simply return undefined. While this is less disruptive since it won’t directly crash your application, it can introduce other issues, such as displaying undefined in your logs. Moreover, depending on how you use the undefined value, it could indirectly lead to application crashes. Consider the following example:Here, invoking .trim() on undefined will trigger a TypeError, potentially crashing your application.In essence, TypeScript aims to safeguard us by designating the type of catchables as unknown. This approach places the onus on developers to determine the correct type of the thrown value, helping to prevent runtime issues.You could always safeguard your code by using optional chaining operators (?.) as shown below:While this approach can shield your code, it employs two TypeScript features that can complicate code maintenance:A preferable approach would be to leverage TypeScript’s type guards. Type guards are essentially functions that ensure a specific value matches a given type, confirming it’s safe to use as intended. Here’s an example of a type guard to verify if the caught variable is of type Error:This type guard is straightforward. It first ensures that the value is not falsy, which means it won’t be undefined or null. It then checks if it’s an object with the expected attributes.This type guard can be reused anywhere in the code to verify if an object is an Error. Here’s an example of its application:By creating a logError function that leverages the isError type guard, we can safely log standard errors as well as any other thrown values. This can be particularly useful for troubleshooting unexpected issues. However, we need to be cautious, as JSON.stringify can also throw errors. By encapsulating it within its own try/catch block, we aim to provide more detailed information for objects, rather than just logging their string representation, [object Object].Furthermore, we can retrieve the stack trace leading up to the point where the new Error object was instantiated. This will include the location where the original value was thrown. While this method doesn’t provide a direct stack trace from the thrown values, it offers a trace from the point immediately after the throw, which should be adequate for tracing back to the problem’s origin.Scoping is likely one of the most common challenges in error handling, applicable to both JavaScript and TypeScript. Consider this example:In this case, because fileContent was defined inside the try block, it’s not accessible outside of it. To address this, you might be tempted to define the variable outside the try block:This approach is less than ideal. By using let instead of const, you’re making the variable mutable, which can introduce potential bugs. Additionally, it makes the code harder to read.One way to circumvent this issue is to wrap the try/catch block in a function:While this approach addresses the mutability problem, it does make the code more complex. This issue could potentially be addressed by creating our own reusable wrapper function. However, before doing so, let’s review the last challenge to ensure we have a comprehensive understanding of all the problems.Here’s an example demonstrating how we would use the new logError function in a scenario where multiple errors could be thrown:You might observe that we’re invoking the .text() API instead of .json(). This choice stems from the behavior of fetch: you can only call one of these two methods. As we aim to display the body content if the JSON conversion fails, we first call .text() and then manually revert to JSON, ensuring we catch any errors in the process. To avoid having cryptic errors like:Uncaught SyntaxError: Expected property name or '}' in JSON at position 42While the details provided by the error will make this code easier to debug, its limited readability can make maintenance challenging. The nesting induced by the try/catch blocks increases the cognitive load when reading the function. However, there’s a way to simplify the code, as shown below:This refactoring fixed the nesting problem but it introduced a new issue: a lack of granularity in error reporting. By removing checks, we become more reliant on the error message itself to understand issues. As we’ve seen from some of the JSON.parse errors, this might not always provide the best clarity.Given all the challenges we’ve discussed, is there an optimal approach to handle errors effectively?Ok, so now that we’ve highlighted some of the challenges surrounding typings and the use of traditional try/catch blocks, the benefits of adopting a different approach become clearer. This suggests that we should seek a superior method than the conventional try/catch blocks for error handling. By harnessing the capabilities of TypeScript, we can effortlessly craft a wrapper function for this purpose.The initial step involves determining how we wish to normalize the errors. Here’s one approach:The primary advantage of extending the Error object is that it behaves like a standard error. Creating a custom error object from scratch might lead to complications, especially when using the instanceof operator to check its type. This is why we set the prototype explicitly, ensuring that instanceof works correctly, especially when the code is transpiled to ES5.Additionally, all the prototype functions from Error are available on the NormalizedError objects. The constructor’s design also simplifies the creation of new NormalizedError objects by requiring the first argument to be an actual Error. Here are the benefits of NormalizedError:Now that we’ve defined how we want to represent normalized errors, we need a function to easily convert unknown thrown values into a normalized error:By using this approach, we no longer have to handle errors of type unknown. All errors will be proper Error objects, equipping us with as much information as possible and eliminating the risk of unexpected error values.Note that E extends NormalizedError ? never is optional. However, it can help prevent mistakenly passing a NormalizedError object as an argument.To safely use NormalizedError object, we also need to have a type guard function:Now, we need to craft a function that will help us eliminate the use of try/catch blocks. Another crucial aspect to consider about errors is their occurrence, which can be either synchronous or asynchronous. Ideally, we’d want a single function capable of handling both scenarios. Let’s begin by creating a type guard to identify promises:With the capability to safely identify promises in place, we can proceed to implement our new noThrow function:By harnessing the capabilities of TypeScript, we can dynamically support both asynchronous and synchronous function calls while maintaining accurate typing. This enables us to utilize a single utility function to manage all errors.Also, as mentioned earlier, this can be especially useful to address scoping issues. Instead of wrapping a try/catch block in its own anonymous self-invoking function, we can simply use noThrow, making the code much more readable.Let’s explore how we can use this approach to refactor the code we developed earlier:There we have it! We’ve addressed all the challenges:You might have also noticed that we didn’t use noThrow on fetch. Instead, we used toNormalizedError, which has more or less the same effect as noThrow but with less nesting. Because of how we built the noThrow function, you could use it on fetch the same way we used it for sync functions:I personally prefer the approach that we took in our example, and I would rather use noThrow in an async context when the code block contains more than a single function. toNormalizedError also doesn’t use a try/catch block under the hood which is better for performance.While the noThrow utility function offers a streamlined approach to error handling, it’s essential to understand that it still leverages try/catch blocks under the hood. This means that any performance implications associated with try/catch will still be present when using noThrow.In performance-critical sections of your code, or “hot code paths,” it’s always a good idea to be judicious about introducing any abstractions, including error handling utilities. While modern JavaScript engines have made significant strides in optimizing try/catch performance, there can still be overhead, especially when used excessively.Recommendations:In essence, while the noThrow function provides a more elegant way to handle errors in TypeScript, it’s essential to be aware of the underlying mechanisms and their potential implications.The combination of the noThrow utility function and the NormalizedError class presents a novel approach to error handling in TypeScript, diverging from the traditional try/catch mechanism native to JavaScript. While this duo offers a streamlined and type-safe error handling experience, developers should be aware of the implications of this paradigm shift:In the ever-evolving landscape of software development, error handling remains a cornerstone of robust application design. As we’ve explored in this article, traditional methods like try/catch blocks, while effective, can sometimes lead to convoluted code structures, especially when combined with the dynamic nature of JavaScript and TypeScript. By embracing the capabilities of TypeScript, we’ve demonstrated a streamlined approach to error handling that not only simplifies our code but also enhances its readability and maintainability.The introduction of the NormalizedError class and the noThrow utility function showcases the power of modern programming paradigms. These tools allow developers to handle both synchronous and asynchronous errors with grace, ensuring that applications remain resilient in the face of unexpected issues.As developers, our primary goal is to create software that’s both functional and user-friendly. By adopting the techniques discussed in this article, we can ensure that our applications not only meet these criteria but also stand the test of time. Happy coding!Templates let you quickly answer FAQs or store snippets for re-use.Wow, thanks for writing yet another great article. I was impressed by how you taught me to improve my wrappers in TypeScript, but I didn't expect that I would relearn how to handle errors properly! Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well Confirm For further actions, you may consider blocking this person and/or reporting abuse Lars Grammel - Sep 3 briankarlsayen - Aug 25 Pierre Bouillon - Sep 4 James Hubert - Aug 24 Once suspended, nbouvrette will not be able to comment or publish posts until their suspension is removed. Once unsuspended, nbouvrette will be able to comment and publish posts again. Once unpublished, all posts by nbouvrette will become hidden and only accessible to themselves. If nbouvrette is not suspended, they can still re-publish their posts from their dashboard. Note: Once unpublished, this post will become invisible to the public and only accessible to Nicolas Bouvrette. They can still re-publish the post if they are not suspended. Thanks for keeping DEV Community safe. Here is what you can do to flag nbouvrette: nbouvrette consistently posts content that violates DEV Community's code of conduct because it is harassing, offensive or spammy. Unflagging nbouvrette will restore default visibility to their posts. DEV Community — A constructive and inclusive social network for software developers. With you every step of your journey. Built on Forem — the open source software that powers DEV and other inclusive communities.Made with love and Ruby on Rails. DEV Community © 2016 - 2023. We're a place where coders share, stay up-to-date and grow their careers.



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

Handling errors in TypeScript the right way

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×