Actions such as calling an API or validating data entered by the user are very common in development and are examples of functions that can give a correct result or fail. Generally to control it in javascript (and other languages) we usually use and create simple exceptions.
They seem like the simplest way to control errors that the application or program we are developing may have. However, as projects and teams grow, scenarios begin to emerge that require something more from us. In large teams, for example, it helps a lot if functions are explicit when indicating whether they can fail, to allow our colleagues to anticipate and manage those errors.
Being explicit with the types of "error" that an action can have will not only help make development much easier. It will also serve as documentation of business rules.
In javascript we have some techniques to achieve this. To go beyond theory, let's consider a real-life example: in a hotel booking app, a user books a room, receives a code, and then must pay for it. When making the payment, the API can currently show us these 3 "error" scenarios:
El código de reserva no existe. El pago es rechazado. El código de reserva ya no es válido.
As we are making an application for the user, in addition to those two cases, you should take into account an extra one:
No hay conexión a internet. (o el servicio no esta disponible)
This function can be called from different components of our application and if it fails, the error must be shown to the user.
With this example in mind, let's review some possibilities of how to handle it
Exceptions are common in many languages, and JavaScript includes some predefined ones (like SyntaxError). When dealing with possible errors, a good practice is to be specific and personalize them.
In js to create an Exception just use the reserved word throw followed by whatever we want (if that's how you read it).
function makeError() { throw "Error string" }
Js is very permissive in that sense, however it is considered bad practice to throw something that is not descended from the Error class that comes in js.
class MyError extends Error { constructor(message) { super(message); this.name = "MyError"; } } function makeError() { throw MyError("") }
As you can see, the error class comes with a property that allows us to describe in greater detail why we are creating the exception (and we can add the properties we want).
Returning to the problem we posed as an example. By applying custom errors we can have control over what to do in each scenario.
El código de reserva no existe. El pago es rechazado. El código de reserva ya no es válido.
With this we not only gain the power to route the flow in different ways but we also distinguish between internal errors of the system (for example the error of some internal dependency that we use within payReservation etc.) from what represent business rules.
This is a very good option, and it meets our objective of controlling the flow according to each case and if someone sees the function they know why it can fail. With that we already gain a lot, however there are some things we must consider with this approach.
The exceptions of a function if they are not controlled within a catch go to the "higher level". To give an example, if you have the function A, which calls B, this in turn calls C and C throws an exception. controlled this will go to B, if B does not control it then it continues until A etc. This depending on your case may be good news. Declaring a possible error by business rule could end up being tedious, since all functions would have to be reviewed for possible exceptions.
Another point to take into account is the developer expiration so valued today. Although tools like JsDoc allow you to describe adding that a method may have an exception, the editor does not recognize it. Typescript on the other hand does not recognize those exceptions when writing or calling the function.
[] **Performance:* throwing and handling exceptions have a (minimal) impact on performance (similar to using break). Although in environments like an app that impact is almost zero.
If we look at the previous case, the exceptions we create are not due to "irreparable" errors, rather they are part of the business rules. When exceptions become common, they stop being really exceptional cases and were designed for that. Instead of throwing exceptions, we can encapsulate the "success" and "error" status in a single object like the following.
No hay conexión a internet. (o el servicio no esta disponible)
If we use typescript (or d.ts to use jsdoc) we could define the types like this.
function makeError() { throw "Error string" }
Applying it to our example. If now our payReservation function returns this object instead of an exception, using JSDoc or Typescript we can specify what type of results we can take (from now on I will put the examples in typescript for simplicity).
This helps the team know in advance what errors the function may return.
class MyError extends Error { constructor(message) { super(message); this.name = "MyError"; } } function makeError() { throw MyError("") }
By applying this we obtain the advantages of the approach with exceptions and also, at development time, the editor will show information about the different "error" cases that can occur.
In fact, this type of concept has been around in programming for a long time, in many functional languages they do not have exceptions and they use this type of data for their error, today many languages implement it. In Rust and Dart, for example, the Result class exists natively, Kotlin's Arrow library also adds it.
There is a certain standard on how to use and implement the Result, so that our code is more understandable for new developers we can rely on those conventions.
Result can represent a success or error state exclusively (it cannot be both at the same time), and allows working with both states without throwing exceptions.
El código de reserva no existe. El pago es rechazado. El código de reserva ya no es válido.
The example uses classes but it is not necessary, there are also simpler implementations, I have one that I usually take to projects where I think I may need it, I leave the link in case you want to see it and/or use it.
If we leave it like this, we don't gain much in relation to the object we created before. That's why it's good to know that it usually implements more methods
For example a getOrElse method to return default values in case of error.
No hay conexión a internet. (o el servicio no esta disponible)
and fold to handle the success/failure flow functionally.
function makeError() { throw "Error string" }
You may also find information on error handling using Either. Result would be an Either with greater context, Either has the value right (right) and left (left). As in English right is also used to say that something is right, it usually has the correct value while the error is on the left, but this is not necessarily the case, result is instead more explicit in relation to where this is the correct value and the error.
Applying it to our example, payReservation would look something like this:
class MyError extends Error { constructor(message) { super(message); this.name = "MyError"; } } function makeError() { throw MyError("") }
[*] A good practice would be to establish a base data type for errors, in the example use string, but ideally it would have a more defined form such as a named object to which more data can be added For example, you can see an example of this here
At first glance it may seem that adding the class is more over-engineering than anything else. But Result is a widely used concept, maintaining conventions helps your team catch it faster and is an effective way to strengthen your error handling.
With this option we explicitly describe what "errors" a function can have, we can control the flow of our application according to the type of error, we get help from the editor while calling the function and finally we leave exceptions for error cases in system.
Despite these advantages, some points must also be taken into consideration before implementing it. As I mentioned at the beginning of this section, Result is native in many languages but not in JS, therefore by implementing it we are adding an extra abstraction. Another point to take into account is the scenario we are in, not all applications will need so much control (On a landing page of an advertising campaign, for example, I would not see the point in implementing the Result). It is worth evaluating whether you can take advantage of all the potential or it will just be extra weight.
In short, handling errors not only improves code quality but also team collaboration by providing a predictable and well-documented workflow. Result and custom exceptions are tools that, used well, contribute to more maintainable and robust code.
In TypeScript, we can take an additional benefit out of Result to ensure that all error cases are covered:
El código de reserva no existe. El pago es rechazado. El código de reserva ya no es válido.
The typeCheck function aims to validate that all possible values of e are checked within the if/else if.
In this repo I leave a little more detail.
The above is the detailed content of Handling errors in Javascript/Typescript: Custom Exceptions and Result. For more information, please follow other related articles on the PHP Chinese website!