Skip to content

Commit e5c3474

Browse files
committed
Error handling and docs
1 parent 5c32685 commit e5c3474

File tree

4 files changed

+806
-26
lines changed

4 files changed

+806
-26
lines changed

decisions/0015-observability.md

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Status: proposed
88

99
We want it to be easy to add observability to production React Router applications. This involves the ability to add logging, error reporting, and performance tracing to your application on both the server and the client.
1010

11-
We always had a good story for user-facing error handling via `ErrorBoundary`, but until recently we only had a server-side error-reporting solution via the `entry.server` `handleError` export. In `7.8.2`, we shipped an `onError` client-side equivalent so it should now be possible to report on errors on the server and client pretty easily.
11+
We always had a good story for user-facing error _display_ via `ErrorBoundary`, but until recently we only had a server-side error _reporting_ solution via the `entry.server` `handleError` export. In `7.8.2`, we shipped an `unstable_onError` client-side equivalent so it should now be possible to report on errors on the server and client pretty easily.
1212

1313
We have not historically had great recommendations for the other 2 facets of observability - logging and performance tracing. Middleware, shipped in `7.3.0` and stabilized in `7.9.0` gave us a way to "wrap" request handlers at any level of the tree, which provides a good solution for logging and _some_ high-level performance tracing. But it's too coarse-grained and does not allow folks to drill down into their applications.
1414

@@ -24,31 +24,54 @@ Adopt instrumentation as a first class API and the recommended way to implement
2424

2525
There are 2 levels in which we want to instrument:
2626

27-
- "router" level - ability to track the start and end of a router operation
28-
- requests on the server handler
29-
- navigations and fetchers on the client router
27+
- handler (server) and router (client) level
28+
- instrument the request handler on the server
29+
- instrument navigations and fetcher calls on the client
30+
- singular instrumentation per operation
3031
- route level
31-
- loaders, actions, middlewares, lazy
32+
- instrument loaders, actions, middlewares, lazy
33+
- multiple instrumentations per operation - multiple routes, multiple middlewares etc.
3234

33-
On the server, if you are using a custom server, this is already possible by wrapping the react router handler and walking the `build.routes` tree and wrapping the route handlers.
35+
On the server, if you are using a custom server, this is already possible by wrapping the react router request handler and walking the `build.routes` tree and wrapping the route handlers.
3436

35-
To provide the same functionality when using `@react-router/serve` we need to open up a new API. Currently, I am proposing a new `instrumentations` export from `entry.server`. This will be run on the server build in `createRequestHandler` and that way can work without a custom server. This will also allow custom-server users today to move some more code from their custom server into React Router by leveraging these new exports.
37+
To provide the same functionality when using `@react-router/serve` we need to open up a new API. Currently, I am proposing a new `instrumentations` export from `entry.server`. This will be applied to the server build in `createRequestHandler` and that way can work without a custom server. This will also allow custom-server users today to move some more code from their custom server into React Router by leveraging these new exports.
38+
39+
A singular instrumentation function has the following shape:
40+
41+
```tsx
42+
function intrumentationFunction(doTheActualThing, info) {
43+
// Do some stuff before starting the thing
44+
45+
// Do the the thing
46+
await doTheActualThing();
47+
48+
// Do some stuff after the thing finishes
49+
}
50+
```
51+
52+
This API allows for a few things:
53+
54+
- Consistent API for instrumenting any async action - from a handler, to a navigation, to a loader, or a middleware
55+
- By passing no arguments to `doTheActualThing()` and returning no data, this restricts the ability for instrumentation code to alter the actual runtime behavior of the app. I.e., you cannot modify arguments to loaders, nor change data returned from loaders. You can only report on the execution of loaders.
56+
- The `info` parameter allows us to pass relevant read-only information, such as the `request`, `context`, `routeId`, etc.
57+
- Nesting the call within a singular scope allows for contextual execution (i.e, `AsyncLocalStorage`) which enables things like nested OTEL traces to work properly
58+
59+
Here's an example of this API on the server:
3660

3761
```tsx
3862
// entry.server.tsx
3963

4064
export const instrumentations = [
4165
{
42-
// Wrap incoming request handlers. Currently applies to _all_ requests handled
43-
// by the RR handler, including:
44-
// - manifest reqeusts
66+
// Wrap the request handler - applies to _all_ requests handled by RR, including:
67+
// - manifest requests
4568
// - document requests
4669
// - `.data` requests
4770
// - resource route requests
4871
handler({ instrument }) {
4972
// Calling instrument performs the actual instrumentation
5073
instrument({
51-
// Provide the instrumentation implementation for the equest handler
74+
// Provide the instrumentation implementation for the request handler
5275
async request(handleRequest, { request }) {
5376
let start = Date.now();
5477
console.log(`Request start: ${request.method} ${request.url}`);
@@ -131,9 +154,48 @@ let router = createBrowserRouter(routes, { instrumentations })
131154

132155
In both of these cases, we'll handle the instrumentation at the router creation level. And by passing `instrumentRoute` into the router, we can properly instrument future routes discovered via `route.lazy` or `patchRouteOnNavigation`
133156

157+
### Error Handling
158+
159+
It's important to note that the "handler" function will never throw. If the underlying loader/action throws, React Router will catch the error and return it out to you in case you need to perform some conditional logic in your instrumentation function - but your entire instrumentation function is thus guaranteed to run to completion even if the underlying application code errors.
160+
161+
```tsx
162+
function intrumentationFunction(doTheActualThing, info) {
163+
let { error } = await doTheActualThing();
164+
// error will be undefined if the underlying handler succeeded,
165+
// or contain the error if it threw
166+
167+
if (error) {
168+
// ...
169+
} else {
170+
// ...
171+
}
172+
}
173+
```
174+
175+
You should not be using the instrumentation logic to report errors though, that's better served by `entry.server.tsx`'s `handleError` and `HydratedRouter`/`RouterProvider` `unstable_onError` props.
176+
177+
If your throw from your instrumentation function, we do not want that to impact runtime application behavior so React Router will gracefully swallow that error with a console warning and continue running as if you had returned successfully.
178+
179+
In both of these examples, the handlers and all other instrumentation functions will still run:
180+
181+
```tsx
182+
// Throwing before calling the handler - we will detect this and still call the
183+
// handler internally
184+
function intrumentationFunction(doTheActualThing, info) {
185+
somethingThatThrows();
186+
await doTheActualThing();
187+
}
188+
189+
// Throwing after calling the handler - error will be caught internally
190+
function intrumentationFunction2(doTheActualThing, info) {
191+
await doTheActualThing();
192+
somethingThatThrows();
193+
}
194+
```
195+
134196
### Composition
135197

136-
Instrumentations is an aray so that you can compose together multiple independent instrumentations easily:
198+
Instrumentations is an array so that you can compose together multiple independent instrumentations easily:
137199

138200
```tsx
139201
let router = createBrowserRouter(routes, {

0 commit comments

Comments
 (0)