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 ).