How to properly handle internal/external API errors in service-, data- & app layers in Next.js?

I’m building a Next application with the following layers:

  • lib/service (async server-only functions doing a fetch from an external api and returning the data)
  • data (async server-only functions that fetches data from various external APIs (calling functions in lib) and merges them into DTOs for the app)
  • route handlers (if we need to make a fetch from the client, a route handler will call a function from the data layer)

I should mention that there will never be direct db access from our Next.js application – only data sources are external APIs.

e.g.

  • lib/external-api/product.ts (could be an api made from the development company)
  • lib/some-other-external-api/product.ts (could be an api made by devs in the client company or a cms etc.)
  • data/product.ts (calls the functions from the above 2 files, transforms it and merges it into DTOs that fits the applications needs)

The app:

  • will never be able to call lib directly – only the data layer is allowed to reach out to the lib functions, treating it just like if it was data from a database.
  • server components calls the data layer directly
  • server actions (can be inside server / client components) calls the data layer directly
  • client components calls has to go through the route handlers to access data from the data layer

So far so good.

After many years of regressing (apparently) as a developer due to building SPAs with GraphQL and Relay, I’ve gotten used to not thinking much about how to handle data fetching properly and just follow clear framework conventions, so now with Next.js 14 and React Server Components I find myself in an awkward situation: I don’t know how to deal with error response details from the external APIs in my lib/service layer.

So the guys building our APIs are pretty awesome returning exact error codes and error object with granular details of what went wrong, and how to handle the errors.

Example of

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "detail": "No product with id ´xyz´ found."
}

where type, title and status is mandatory and detail is optional.

Sometimes if the error is related to validation there might also be an errors field in addition to type, title, status and others:

"errors": {
  "xyz": [
    "The field Xyz must match the regular expression asd"
  ]
},

And sometimes in case of a conflict 409, the api response object might look like this:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.10",
  "title": "Conflict",
  "status": 409,
  "detail": "Cart includes conflicted items.",
  "conflicts": [
    {
      "productId": "xyz",
      "quantity": 1,
      "productName": "Some product name",
      "problem": "product_price_differs_in_selected_store",
      "oldPrice": {
        "currency": "USD",
        "value": 13.37
      },
      "newPrice": {
        "currency": "USD",
        "value": 23.37
      },
    }
  ],
}

The 409 conflict error object carries all those details because I need to react to the conflicted items and present a UI where the customer will need to verify that they accept the price change (or whatever the change may be – could also be the removal of products in the cart).

And then it struck me that I’ve always been used to not thinking about these things due to heavily opinionated graphql tech on both server and client side – or back when I did CRUD apps with REST endpoints I don’t recall ever having handled and responded properly to different error codes and error responses such as a 409.

I’ve read and watched a lot of tutorials/videos/examples of error vs exception handling, try/catch or callback based or hybrid versions. I know some people prefer to always just throw anything as an exception and some never throws exceptions and claims it ruins the control flow (eg. neverthrow, fp-ts etc).

After hearing different opinions I’ve concluded that it makes sense to me to only throw exceptions in exceptionally unexpected cases or when recovering from errors might not be within the users control.

Of course I need to throw a 404 (with notFound) in the app layer or the data layer (called by the app layer) to trigger the not found page in Next.js, but besides that one I’m kind of confused.

Some claim it’s best to handle the error and return a tuple, array or object containing either data or the error, eg { data: {}, error: {} }

On the other hand some argue that by throwing an exception early, you force the developer to handle the error early on, and not forget to do so by mistake.

So now, after investigating it for weeks, I have more questions and find myself confused almost to the point where I can’t write a single line of code anymore, since I find myself so deep down in this rabbit hole of error handling, which is quite overwhelming as you start doubting yourself:

  • in the lib layer, when fetching data from external APIs and returning the raw response what errors should I handle here (if any) or should I just forward the unhandled response and not handle errors here?
  • Or should I return if response.ok and otherwise handle in if conditions for all the different error codes and make custom error classes that extend from Error and map each error response to these custom error classes, to carry on their data, and rethrow these?
  • Or can I just avoid creating them and instead return the simple error object? (this feels almost like not handling the error at this layer, and has the problem that the developer might forget or be unaware to check the error object and handle it at the data layer, whereas an exception forces the developer to handle it.
  • what type of errors do I handle in the lib and data layer exactly?
  • And do I serve both {data:{}, error:{}} responses from the data layer when the server components or server actions invokes it from the app layer? Or should the data layer throw an exception forcing the app to catch the exception and handle it using its error details?
  • is extending Error with very detailed error object data even what it’s intended for?

I see a lot of overload in making a lot of custom error classes that inherits from Error just to be able to throw/rethrow custom exceptions with additional details other than the message. On the other hand it forces devs to handle the exceptions. Also if you miss handling some exception they will be picked up by the Next.js error boundaries, which can both be a good or bad thing.

Here’ what I’m leaning towards:

Some errors are recoverable, some are not. Some are recoverable from the client-side (user input, or programmatic), some are recoverable from the server side. Error meta data besides the message is only necessary if the errors are client-side recoverable.

This means that for all 500 – internal server errors – we don’t need to consider the error details.

This leaves the 400 range.

And you can further narrow it down to just a handful. In my case it’s only for the 409 conflict and the 400 validation errors that I need to send further error details to be able to handle it in the application.

The rest can just be handled as a string in the frontend, after logging has occured in the backend.

lib layer:

  • call fetch to get response data from each endpoint.
  • the response might be either the expected data with status 200 (response.ok)
  • if the response is not “ok” the possibilities are most likely in the response in the form of an error response object.
  • Finally the error might not be in the response, if the endpoint was not called correctly, or if there were and issue serializing the response. In this case I think logging the error and simply returning a simple Error object (not to be confused with the Error class) { status: number, title: string, detail?: string } is the right thing to do, since there is no more we can do at this point to try to recover.
  • if the error was in the response object, the type of error was known and handled by the external API, in which case I will handle them in 1 of 2 ways:
  • the client-recoverable (errors such as conflict/validation, that holds additional information to fix the error) should be modelled in a zod schema just like the data and it should be included as a possible return type of the function eg. const changeCartStore = async (): { data: undefined, error: ConflictError | ValidationError | Error } => {} etc.
  • bad requests, serializable etc. + 5xx errors should just be returned as simple { error: { message: 'error_cart_store_change' } } (translation_key) since there is no more we can do about them and they’ve already been logged in the backend.

Dilemma/confusion: Do I simply return { data: ..., error: {} } here or do I throw/rethrow instances of the Error class here? The consumer will always be the same – the data layer only?

data layer:

  • then in the data layer, when calling the functions in the lib layer the signature will be like const { data, error } = await changeCartStore() – depending whether data/error data needs to be merged or transformed, it either just forwards the data and errors in terms of simply returning both to the application – instead of throwing them to trigger error boundaries and not-found errors unless you catch them in the server components .
  • since the primary function of the data layer in most cases (other than the example of changing cartStore) is to transform, merge and make targeted DTOs for the UI, there might be added DataTransformationError to the error return type, in case

Dilemma/confusion: Do I simply return { data: ..., error: {} } here or do I throw/rethrow instances of the Error class here, and catch them in the consumers? The consumers will always be either the server components, server actions or a route handler.

app:

  • in the server components, server actions and route handlers the data layer is invoked like this const { _, error } = await changeCartStore(cartId, storeId) where I need to check the error for any error data to respond to.

Dilemma/confusion: Another approach is to call data layer functions that throws means that I need to either handle errors in the UI with await getSomeFuncThatThrowsFromDataLayer().catch(err => ...) or not handle them but leave it up to the errorBoundaries completely (very bad idea imo). The good thing about throwing errors in the data layer forces you to handle the errors in the frontend, it integrates nicely with React Query and useOptimisticQuery. The bad thing is that all the logic for handling various error is now moved to the rendering phase. You can mitigate this by moving the logic to a helper function, but it’s still not clear to me if the data layer should throw/rethrow errors to the app (server components, server actions an route handlers).

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật