For UI routes, error handling typically involves using error boundaries to catch and display errors in a user-friendly manner, preventing the entire application from crashing due to a single component error.
There are two main ways to add error handling to UI routes, either by exporting
an ErrorFallback
component from the route file or by manually adding an error
boundary to your component.
The simplest way to add error handling to a UI route is to export an
ErrorFallback
component from the route file. The framework will automatically
wrap the route's default export with an error boundary using this fallback.
Example:
import { FallbackProps, HttpError } from "@udibo/react-app";
export default function BlogPost() {
// ... component logic
}
export function ErrorFallback({ error }: FallbackProps) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
</div>
);
}
// Optionally, you can specify a custom boundary name
export const boundary = "BlogPostErrorBoundary";
In this example, if an error occurs within the BlogPost
component, the
ErrorFallback
component will be rendered instead.
For more control over error handling, you can manually add an error boundary to
your component using the ErrorBoundary
component or the withErrorBoundary
higher-order component.
Using ErrorBoundary
:
import { DefaultErrorFallback, ErrorBoundary } from "@udibo/react-app";
export default function Blog() {
return (
<ErrorBoundary
FallbackComponent={DefaultErrorFallback}
boundary="BlogErrorBoundary"
>
{/* Blog content */}
</ErrorBoundary>
);
}
Using withErrorBoundary
:
import { DefaultErrorFallback, withErrorBoundary } from "@udibo/react-app";
function Blog() {
// ... component logic
}
export default withErrorBoundary(Blog, {
FallbackComponent: DefaultErrorFallback,
boundary: "BlogErrorBoundary",
});
In the browser, any errors that occur within a route will be caught by the nearest error boundary. When rendering on the server, the errors will have a boundary key added to them to indicate which error boundary they should be associated with during rendering. If a route throws an error, it will default to the nearest route's error boundary.
In the following example, any errors thrown in the route will automatically have
the boundary key set to the boundary for that route. If the route path is
/blog/:id
and the UI route file doesn't export a boundary constant, any errors
thrown will have the /blog/:id
boundary added to them. If the UI route file
does export a boundary constant, that will be used instead of the default.
import { HttpError } from "@udibo/react-app";
import { Router } from "@udibo/react-app/server";
import { getPost } from "../../services/posts.ts";
import type { PostsState } from "../../models/posts.ts";
export default new Router<PostsState>()
.get("/", async (context) => {
const { state, params } = context;
const id = Number(params.id);
if (isNaN(id) || Math.floor(id) !== id || id < 0) {
throw new HttpError(400, "Invalid id");
}
state.app.initialState.posts = {
[id]: getPost(id),
};
await state.app.render();
});
Routes can have as many error boundaries as you need. If you want an error on
the server to be caught by a specific boundary when doing server-side rendering,
you'll need the error to be thrown with the boundary key set to the name of the
boundary you want to catch the error. This can be done automatically by using
the errorBoundary
middleware. The following example shows how to use the
errorBoundary
middleware to catch errors in a route. Now instead of the errors
having the routes boundary key added to them, it will have the
BlogErrorBoundary
boundary key added to them instead.
import { HttpError } from "@udibo/react-app";
import { Router } from "@udibo/react-app/server";
import { getPost } from "../../services/posts.ts";
import type { PostsState } from "../../models/posts.ts";
export default new Router<PostsState>()
.use(errorBoundary("BlogErrorBoundary"))
.get("/", async (context) => {
const { state, params } = context;
const id = Number(params.id);
if (isNaN(id) || Math.floor(id) !== id || id < 0) {
throw new HttpError(400, "Invalid id");
}
state.app.initialState.posts = {
[id]: getPost(id),
};
await state.app.render();
});
Alternatively, you can throw an HttpError
with the boundary key set to the
name of the boundary you want to catch the error. In the following example, the
invalid id error will be caught by the BlogErrorBoundary
boundary instead of
the default boundary. Any other errors will still be caught by the route's
boundary.
import { HttpError } from "@udibo/react-app";
import { Router } from "@udibo/react-app/server";
import { getPost } from "../../services/posts.ts";
import type { PostsState } from "../../models/posts.ts";
export default new Router<PostsState>()
.get("/", async (context) => {
const { state, params } = context;
const id = Number(params.id);
if (isNaN(id) || Math.floor(id) !== id || id < 0) {
throw new HttpError(400, "Invalid id", { boundary: "BlogErrorBoundary" });
}
state.app.initialState.posts = {
[id]: getPost(id),
};
await state.app.render();
});
If the error could come from somewhere else that doesn't throw an HttpError with
a boundary, you can catch and re-throw it as an HttpError with the correct
boundary like shown in the following example. It is doing the same thing as the
error boundary middleware, but only applying to the code within the try
statement. Any errors thrown within there will have their boundary set to
BlogErrorBoundary
. Any errors thrown outside of there will have their boundary
set to the route's boundary.
import { HttpError } from "@udibo/react-app";
import { Router } from "@udibo/react-app/server";
import { getPost } from "../../services/posts.ts";
import type { PostsState } from "../../models/posts.ts";
export default new Router<PostsState>()
.get("/", async (context) => {
const { state, params } = context;
const id = Number(params.id);
if (isNaN(id) || Math.floor(id) !== id || id < 0) {
throw new HttpError(400, "Invalid id");
}
try {
state.app.initialState.posts = {
[id]: getPost(id),
};
} catch (cause) {
const error = HttpError.from<{ boundary?: string }>(cause);
if (isDevelopment()) error.expose = true;
error.data.boundary = "BlogErrorBoundary";
throw error;
}
await state.app.render();
});
In API routes, error handling involves catching and properly formatting errors, setting appropriate HTTP status codes, and potentially logging errors for debugging purposes. The framework provides utilities to streamline this process and ensure consistent error responses across your API.
By default, errors thrown in API routes are caught and handled automatically.
The response body is set to an ErrorResponse
object representing the error.
This object typically includes:
status
: The HTTP status codemessage
: A description of the errordata
: Additional error details (if provided)
These errors are also logged as API route errors, which can be useful for debugging and monitoring purposes.
The framework provides an HttpError
class that you can use to create and throw
custom errors in your API routes. Here's how you can use it:
import { HttpError } from "@udibo/react-app";
// ...
if (someErrorCondition) {
throw new HttpError(400, "Invalid input");
}
You can use the expose
property to control whether the error message is
exposed to the client:
throw new HttpError(400, "Invalid input", { expose: false });
When expose
is set to false
, the client will receive a generic error message
instead of the specific one you provided. This is useful for hiding sensitive
information or internal error details from users. HTTP errors with a status code
of 500 or greater are not exposed to the client by default, they will only be
exposed if you explicitly set expose
to true
. The inverse is true for HTTP
errors with a status code between 400 and 499, they will be exposed to the
client by default, but you can set expose
to false
to hide them.
Non-HTTP errors will be converted into an HttpError with a status code of 500, with the original error being set as the cause. The cause will be logged but not exposed to the client.
You can add extra context to your errors by including additional data:
throw new HttpError(400, "Form validation failed", {
field: "email",
reason: "Invalid format",
});
This additional data will be included in the ErrorResponse
object sent to the
client. It's important to note that this data is shared with the client, so be
careful not to include any sensitive information.
If you need more control over error handling, you can override the default behavior by adding custom middleware to the root of your API routes. Here's an example of how to do this:
import { Router } from "@udibo/react-app/server";
import { ErrorResponse, HttpError } from "@udibo/react-app";
import * as log from "@std/log";
export default new Router()
.use(async ({ request, response }, next) => {
try {
await next();
} catch (cause) {
const error = HttpError.from(cause);
log.error("API Error", error);
response.status = error.status;
const extname = path.extname(request.url.pathname);
if (error.status !== 404 || extname === "") {
response.body = new ErrorResponse(error);
}
}
});
This middleware catches any errors thrown in subsequent middleware or route
handlers. It converts the error to an HttpError
, sets the appropriate status
code, and formats the response body. You can customize this further to fit your
specific error handling needs, such as integrating with error tracking services
or applying different handling logic based on the error type.