Add error management abilities to interaction methods of Contract interface
When making a call to bundleInteraction with strict flag set to true, multiple kinds of error can be thrown. I experienced two of them: a contract interaction error and a gateway error. The issue is that except for the message attached to their Error object, there is currently no way of telling them apart, and no clear indications on what other kind of error (if any) could also be thrown.
Maybe instead of throwing a generic Error object it could throw a custom one with at least a field (a string literal or an Enum variant ...) that would describe the nature of the error so the code consuming the error would be able to act accordingly? It would also allow users of the library to easily tell what could go wrong within this method or others by looking at said string literal union/enum/... .
Here's how I usually go about for this:
// Define the different kinds of error for a bundle interaction
enum BundleErrorKind {
InteractionFailed = "InteractionFailed",
UnrecognizedGatewayStatus = "UnrecognizedGatewayStatus",
}
// Or define the error kind like this, which achieves more or less the same thing while being less verbose:
// type BundleErrorKind = "interactionFailed" | "unrecognizedGatewayStatus";
// Define the custom error object with the "error kind" value attached
class BundleError extends Error {
kind: BundleErrorKind;
constructor(kind: BundleErrorKind, message = "") {
super(`${kind}: ${message}`);
this.name = "BundleError";
this.kind = kind;
Error.captureStackTrace(this, BundleError);
}
}
// Then we can throw this custom error and attach an "error kind" value to it:
function bundleInteraction() {
try {
// some code that calls the gateway and gets a bad status from it
} catch {
throw new BundleError(BundleErrorKind.UnrecognizedGatewayStatus);
}
}
// And eventually, it's easy to see where the error comes from by checking the `kind` field
try {
bundleInteraction();
} catch (error) {
if (error instanceof BundleError) {
switch (error.kind) {
case BundleErrorKind.InteractionFailed:
break;
case BundleErrorKind.UnrecognizedGatewayStatus:
break;
default:
exhaustive(error.kind);
}
} else {
// handle other errors
}
}
So after doing some research on the subject, I am not satisfied anymore with the pattern I described above for reasons I'm going to explain below.
What I want to achieve with the error handling story in Warp is two fold. It should allow users to easily:
- Catch errors coming from the library and tell them apart them
- Tell what could go wrong when calling a function from Warp
In the current state of Warp, these two points are not addressed, which is why I suggested an error handling pattern in my previous. However, what I suggested only handles point 1 very poorly and doesn't address point 2 at all. The only way for point 2 to be satisfied would be to manually maintain documentation for every functions that would cause an error either by throwing it themselves or by calling other functions that could throw an error. This means that if the documentation isn't perfectly maintained, it's going to be very hard for the users to know where they should handle what error without having a deep knowledge of Warp's code base. Realistically, I don't think such an error-handling-related documentation can be maintained efficiently. And it's also going to be difficult for anyone developing Warp to keep a mental map of where errors can happen and what kind of error can happen where.
Consequently, while it looks like this solution could, in appearance, easily be integrated into Warp without introducing any significant breaking change, for it to be actually usable in a meaningful way it's going to end up being a big burden on the developers and probably cause a lot of confusion if a bug happens in the error-handling process (a function throw something that isn't described in the function's doc, or the opposite, etc...).
A big part of the problem comes from JS and TS themselves. When we catch an error, the error's type is always unknown, so it becomes the user's burden to find out exactly what is actually being thrown. There is no integration or error-handling in Typescript's type system. Although, it is still possible to actually use Typescript's type system in order to handle errors, by using a completely different pattern: do not throw errors, return them. This pattern is actually what is used in a lot of different languages (like Rust, Haskell, ...), and apparently comes from the functional programing world. There are a couple ways to implement this in Typescript, either manually or by using a library. Having researched the different available options, I feel like using the Result type from https://github.com/supermacro/neverthrow works pretty well.
There are definitely a few pain points with this solution as well, although I believe they are worth it as they completely address the two objectives I listed at the beginning of this post. By encoding the errors a function can make into its return type, it forces the users to either handle these errors or consciously ignore them. It also allows them (and Warp developers) to know just by looking at the signature of a function exactly what kind of error can originate from it. Another amazing point of this solution is that it greatly simplifies refactoring. If a change to a function make it able to make a new kind of error, coupled with exhaustive switch checks, it will cause Typescript to tell us exactly where we have to handle this new error and also what other functions signatures we have to change (what functions call the one that returns a new error).
The pain points I mentioned for this solution are mainly that it's going to change a lot of function signatures, introducing a large amount of refactoring and breaking changes in the way these functions are consumed (inside Warp and also for users of the lib). In order to demonstrate all of what I have been writing about in this post, I have prepared two PR: one with the first solution and another with the second solution (using the neverthrow library), so that it's going to be easier to understand their reach and what could work better for Warp. I am going to link both of these PR to this issue so they can be found easily.