4

Is there a substantial difference between throwing vs returning an error from a service class?

class ProductService {
    async getProduct(id) {
        const product = await db.query(`...`)

        if (!product) {
            throw new ProductNotFoundError()
        }

        return product
    }

    // VS
    
    async getProduct(id) {
        const product = await db.query(`...`)

        if (!product) {
            return {
                data: null,
                error: new ProductNotFoundError()
            }
        }

        return {
            error: null,
            data: product
        }
    }
}

In this example, I'd like to raise an error message if a product is not found ( supposedly returning null is not enough, maybe I need to also provide additional info why the product is not found ).

As a general design principle, is there a preferred approach, are there any substantial pros/cons of both approaches?

I've been doing the throw new Error example, but then the code gets riddled with try/catch or .then/.catch statements, which make it somewhat harder to reason about.

What I don't particularly like about throwing errors is.. they are not unexpected. Obviously, I expect that a product might not be found, so I added a piece of logic to handle that part. Yet, it ends up being received by the caller the same way, for example, a TypeError is received. One is an expected domain error, the other one is unexpected exception.

In the first scenario, if I return an object with data and error properties ( or I can return an object with methods like isError, isSuccess and getData from each service, which is similar ), I can at least trust my code to not raise exceptions. If one happens to arise, then it will be by definition unexpected and caught by the global error handlers ( express middleware, for example ).

2
  • I have precisely the same question. You’ve articulated it perfectly. Im leaning towards the second way myself and considered returning Promise<Product | ProductError> (slight variation on a single object). I think there will be cases to use try/catch. Inside the service when using transactions is one example. Another example might be unique constraint on usernames. Using a try/catch in the service for that and returning the appropriate error. Commented Aug 9, 2023 at 4:24
  • For more reading on this topic, search for discussions about try/catch in Golang. Go returns errors and doesnt have try/catch because they wanted to keep errors truly exceptional and force developers to handle them. There is plenty of discussion for and against this aspect of go. Commented Aug 9, 2023 at 13:12

1 Answer 1

5

Is there a substantial difference between throwing vs returning an error from a service class?

Big difference. throw in an async function causes the promise that is returned from any async function to be rejected with that exception as the reject reason. Returning a value becomes the resolved value of the promise.

So, the big difference is whether the promise returns from the async function is resolved with a value or rejected with a reason.

As a general design principle, is there a preferred approach, are there any substantial pros/cons of both approaches?

Most people use promise rejections for unusual conditions where the caller would typically want to stop their normal code flow and pursue a different code path and use return values for something that is likely to continue the normal code flow. Think about chained promises:

x().then(...).then(...).catch(...)

If the condition should go immediately to the .catch() because something serious is busted, then a throw/rejection makes the most sense in x(). But, if the condition is just something to typically handle and continue the normal execution, then you would want to just return a value and not throw/reject.

So, you try to design x() to be the easiest for the expected use. A product not being found in the database is an expected possible condition, but it also might be something that the caller would want to abort the logic flow they were in, but the treatment of that condition will certainly be different than an actual error in the database.

As a reference example, you will notice that most databases don't treat a failure to find something in a query as an error. It's just a result of "not found".

Sign up to request clarification or add additional context in comments.

15 Comments

Thanks for the input. Yes, I am aware of these things. However, if I follow the second approach across my application, I would never write a try/catch statement in my own code, the only try/catch statements would be in the request middleware. That way, if an exception happens to arise, then it is supposed to break the remaining part of my code, because I wasn't expecting it. The question is, if an error is an expected case, sooner or later ( a product is not found ), then isn't it part of a successful response? It is, after all, an expected response.
@DiyanSlavov - Please make sure you see what I've recently added to my answer. I don't understand what you're talking about with middleware as there is no middleware code shown in your question. You would pretty much always have to code a .catch() somewhere because true database errors can happen (perhaps unexpectedly).
Well, in a NodeJS application you need to have some error handling with try/catch blocks, or you'd end up killing the process. If I don't want to write them myself, then a good idea would be to set up middleware, that catches any errors during the lifecycle of the request, and responds with Status: 500 if one happens to arise. I am also aware that databases don't treat no finding anything as an error, but there aren't many reasons that something might not exist in the database. In my example, a product may be disabled, soft-deleted or not created at all ( I just made these up ).
I included a Service example, because this is the particular use-case that I'm interested in. If I was designing something else, it does make more sense to throw an error. But aiming to design a predictable service domain layer, should an expected domain error be raised the same way that a regular JavaScript error would be? My second example assumes no try/catch statements in the code whatsoever. Any errors that are expected are part of a successful response, anything else - something crashed, and if there arent any catch statements up, it will be handled by the Express Middleware ( 500 )
Yes, I am aware that it is sort of a personal preference. At the moment, there are try/catch blocks, as I'm using the first approach, its just that the boilerplate is too much at some points and I was wondering if this alternative was possibly a better approach. I will further research this subject for pros and cons, as it is not a yes or no situation, and make up my own mind. Thank you for the insight.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.