DEV Community

Cover image for TypeScript with Go/Rust errors? No try/catch? Heresy.

TypeScript with Go/Rust errors? No try/catch? Heresy.

Mateusz Piórowski on July 23, 2023

So, let's start with a little backstory about me. I am a software developer with around 10 years of experience, initially working with PHP and then...
Collapse
 
skyjur profile image
Ski • Edited

Wrapping JSON.stringify is exercise in futility. You cannot recover from this error. Then why even handle it? Fix the bug instead of handling bugs and adding workarounds. You only need to handle errors if you have decided on particular fallback logic for the specific error. If you don't have specific ideas for fallback then you shouldn't have try/catch for that part of code. You also shouldn't need specific error messages on every single function call as stacktrace should be enough for you to be able to debug it. You only need 1 high level try/catch for any kind of unexpected error that shows "sorry something went wrong' to user" and logging/stack-trace that is good enough to debug and fix your errors.

Collapse
 
buffos profile image
Kostas Oreopoulos

Sorry but you are wrong. Handling the error at the origin of very different than debugging through trace stack.

Network errors, db errors, validation errors, file system errors, do not need a stack trace.
They might need a retry strategy, you might need to respond differently.
An smtp server error could be handled by retrying, using an alternate server or using a message broker to retry in future.

A top level only error handling is very bad and sloppy.

Collapse
 
skyjur profile image
Ski

As you can see I am not really focusing on network errors but on JSON.stringify. The OP advocates blind try/catch on everything. Network errors sure need to be handled. Yet again probably not at every single case where fetch is used. Application should really only need one handler for network connection errors and feature/domain specific functionality should not be plagued with error handling for networking layer issues.

Thread Thread
 
buffos profile image
Kostas Oreopoulos • Edited

I strongly disagree. Even with Json stringify.

You get data from a database and fails with Json stringify. It should not under normal circumstances. A stack trace does not help, you need good logs,
I do not know if you have ever used the efk stack of something similar, but good logs are as important as stack traces

Your goal is not only to inform the user. Logs are your friend

Thread Thread
 
skyjur profile image
Ski • Edited

Sure good logs are helpful too. I didn't said they are not. Agree that logs are necessary. But good logs is a separate question. Adding a message to JSON.stringify failure as in "stringify failed", is not helpful at all. The error from JSON.stringify is already useful enough as it says Uncaught TypeError: Do not know how to serialize a BigInt. Also stacktrace points to line where JSON.stringify is. If you just don't do touch it and don't try to reinvent errors there is already quite enough information to know how to debug the problem. Now it becomes hard when you start wrapping this with your own error messages and hiding stacktraces. In OP's article the stack trace will be gone and instead of well known error you will get custom error, and the way he's done it - because he's trying to remove all exceptions and replace it with railway programming - stack trace might be so screwed or non existent that it will be many layers away from where the error actually happened. And it doesn't end here the way he's doing it there might be no error reported at all and he might just return 400 over api confusing the API where instead it is a applicatoin bug that only deserves 500 response and alerts triggered. When your code is so screwed like that then indeed only excessive logging can save you from spending days of debugging the problem but even with them it might be difficult.

Thread Thread
 
buffos profile image
Kostas Oreopoulos

Golang uses error wrapping, and unwrapping, and is very very effective.

Stack traces are for noon recoverable errors, mainly.

With errors as values you have
a. The ability to control if you recover or not
b. How to respond to the error.
c. If you wish to hide implementation details of not.

Generally, is good practice, each layer of your application to have it's own errors.
You can use error wrapping for some part of your app, for inner logging and some other errors for the end user to hide implementation details.

In any case,I really like the idea of errors as values.

Collapse
 
vseplet profile image
Vsevolod

Great article and good examples! I would also add a recommendation to design functions initially in such a way that the list of possible returned errors is clear. For example, through the Either monad.

Collapse
 
mpiorowski profile image
Mateusz Piórowski

You made me discover the Either monad :) Just another example that you never stop learning.

Collapse
 
vseplet profile image
Vsevolod • Edited

It's amazing! I think Either would be a great addition to your work. safe T for wrappers of already existing unsafe code and Either L, R for implementing safe code natively

Thread Thread
 
mpiorowski profile image
Mateusz Piórowski

More i dive into it, the more interesting it looks like, but this is like a material for another article :D It's not just a "few lines" more to make it work and make it understandable ;p

Thread Thread
 
vseplet profile image
Vsevolod

I've spent quite a bit of time thinking about and implementing these monads in TypeScript in a way that's really pretty and understandable. If you want, I will be glad to share and work on the article with you)

Thread Thread
 
mpiorowski profile image
Mateusz Piórowski

Yeah, that's sounds rly interesting. First i need to dive into this topic and see if this will suit me. But if this will be as interesting as it seems to be, i would glady make another article with Your help.

Collapse
 
rob117 profile image
Rob Sherling

Absolutely fantastic article. Interesting way to handle errors - the need to remember to wrap promises in safe is rough, but the tradeoff gets rid of big try-catch blocks and messy error handling. And the Either monad comments are educational as well.

10/10 quality post.

Collapse
 
jsardev profile image
Jakub Sarnowski

Amazing article, thank you! :)

Collapse
 
lzzqwe profile image
liudehua

isExternal

Collapse
 
skyjur profile image
Ski

I see in many cases of your code you are deciding to return error code 400. Yet in many cases I see that no user-error can produce it and they do seem more like programmer/server errors that should result in 500. And if they do result in 500 then you should consider to return generic error message, and log the error, to avoid exposing application internals over api.

Collapse
 
milaabl profile image
milaabl

Why not use a package like true-myth? Particularly for fetching and API integrations, @zodios/react, @zodios/core & zod would work best together for type-safe error handling & return types handling & validation.

Collapse
 
mickmister profile image
Michael Kochell • Edited

So safeAsync takes a promise. What if the function that was called to get the promise throws an error, before it gets to its return statement that returns the initial promise? Then the exception would happen before safeAsync has a chance to do its error catching.

Collapse
 
marcio199226 profile image
oskar

nice but I prefer github.com/scopsy/await-to-js approach is more straighforward though

Collapse
 
mpiorowski profile image
Mateusz Piórowski

True, the general idea is almost the same, they have a wrapper that return a tuple with err / data. But for me i prefer the "forced" approched, which, if i am correct, they do not implement. You can access the response without checking the error.

Collapse
 
marcio199226 profile image
oskar

"You can access the response without checking the error."
Can you explain better what you mean?
Unfortunately we can't ignore return values in js such us in go by using "_" as var's name...

Thread Thread
 
mpiorowski profile image
Mateusz Piórowski

So looking at the example in the repo, You can do sth like that:

[ err, user ] = await to(UserModel.findById(1));
doSomethingWithUser(user); <- You can do anything with user, without checking the error
Enter fullscreen mode Exit fullscreen mode

Looking at my example:

const response = await safe(fetch("https://example.com"));
doSomethingWithResponseData(response.data); <- i cannot do this, the lsp wont let me, not without checking the error first
Enter fullscreen mode Exit fullscreen mode

But maybe their lib works diffrently, i didnt dive deep, just looking at example so i could be wrong.

Collapse
 
robertsandiford profile image
RobertSandiford

It would be pretty easy to deal with exceptions through typed errors. Functions show be marked with the type of error they throw, and a function that can throw should be colored in IDEs so we know about it. Most of it this would be automatically inferred.

But this is something that would need to be added to TS, so monads are a good option for those just using TS

Collapse
 
vitalicset profile image
Vitali Haradkou

Good article, such JS error handling misunderstanding motivates me to write Rust-like library, since it's just better on a mile.
github.com/vitalics/rslike