Skip to content

Commit

Permalink
feat: Error handling for resources
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarParra authored Dec 20, 2024
1 parent cae2013 commit f4f7351
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 180 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:
branches: ["main", "develop"]
workflow_dispatch:

permissions:
contents: write

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -20,3 +23,7 @@ jobs:
- run: npm ci
- run: npm run build --if-present
- run: npm test
- uses: EndBug/add-and-commit@v7
with:
commit: "--allow-empty"
default_author: github_actions
69 changes: 62 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,11 @@ The preferred way of reading a signal is through the `signal.value`.

## Error Handling

When unhandled errors occur in a `computed` or `effect`, by default, the error will be logged to the console through
a `console.error` call, and then the error will be rethrown.
When unhandled errors occur in a `computed`, `effect` or `resource`,
by default, the error will be logged to the console through a `console.error` call, and then the error will be rethrown.

If you wish to know which `computed` or `effect` caused the error, you can pass a second argument to the `computed` or
`effect` with a unique identifier.
If you wish to know where the error is coming from, you can pass a second argument to the `computed`,
`effect` or `resource` with a unique identifier.

```javascript
$computed(
Expand All @@ -242,6 +242,14 @@ $effect(
},
{ identifier: "test-identifier" }
);

$resource(
asyncFunction,
{},
{
identifier: "test-identifier"
}
);
```

This value will be used only for debugging purposes, and does not affect the functionality otherwise.
Expand All @@ -250,7 +258,7 @@ In this example, the test-identifier string will appear as part of the console.e

### Custom Error Handlers

Both computed and effect signals can receive a custom `onError` property,
`computed`, `effect`, and `resource` signals can all receive a custom `onError` property,
that allows developers to completely override the default functionality that logs and rethrows the error.

#### Effect handlers
Expand Down Expand Up @@ -316,6 +324,53 @@ $computed(
);
```

#### Resource handlers

For `resource` handlers, you can pass a function with the following shape:

```typescript
(error: unknown, previousValue: T | null, options: { initialValue: T | null, identifier: string | symbol }) =>
AsyncData<T> | void

// Where AsyncData looks as follows
// {
// data: T | null;
// loading: boolean;
// error: unknown | null;
// };
```

Where you can return nothing, or a value of type `AsyncData<T>`.
`AsyncData` is the shape that all resources take, and it contains the data, loading state, and error state.
This allows you to provide a "fallback" value, that the computed value will receive in case of errors.

As a second argument, you will receive the previous value of the resource (or null if there is none), which can be useful to provide a
fallback value based on the previous value.

The third argument is an object with the received identifier as well as any initial value that was provided to the
resource when it was created.

Example

```javascript
function customErrorHandlerFn(error, _previousValue, _options) {
// custom logic or logging or rethrowing here
return {
data: "fallback value",
loading: false,
error: error
};
}

$resource(
asyncFunction,
{},
{
onError: customErrorHandlerFn
}
);
```

## Tracking objects and arrays

By default, for a signal to be reactive it needs to be reassigned. This can be cumbersome when dealing with objects
Expand Down Expand Up @@ -512,11 +567,11 @@ export default class ContactList extends LightningElement {
}
```

Data from a resource signal comes in the following format:
Data from a resource signal comes in as a read-only signal in the following format:

```typescript
type AsyncData<T> = {
data: ReadOnlySignal<T> | null; // The data fetched from the server. It is null until the data is fetched
data: T | null; // The data fetched from the server. It is null until the data is fetched
loading: boolean; // A boolean that indicates if the data is being fetched
error: unknown | null; // An error object that is populated if the fetch fails
};
Expand Down
35 changes: 25 additions & 10 deletions force-app/lwc/signals/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function $computed(fn, options) {
() => {
if (options?.onError) {
// If this computed has a custom error handler, then the
// handling occurs in the computed function itself.
// handling occurs here, in the computed function itself.
try {
computedSignal.value = fn();
} catch (error) {
Expand Down Expand Up @@ -250,7 +250,22 @@ function $signal(value, options) {
delete returnValue.unsubscribe;
return returnValue;
}
function defaultResourceErrorHandler(error, _previousValue, options) {
const errorTemplate = `
LWC Signals: An error occurred in a reactive function \n
Type: Resource \n
Identifier: ${options.identifier.toString()}
`.trim();
console.error(errorTemplate, error);
}
function $resource(fn, source, options) {
const {
initialValue = null,
optimisticMutate = true,
fetchWhen = () => true,
identifier = Symbol(),
onError = defaultResourceErrorHandler
} = options ?? {};
function loadingState(data) {
return {
data: data,
Expand All @@ -259,17 +274,14 @@ function $resource(fn, source, options) {
};
}
let _isInitialLoad = true;
let _value = options?.initialValue ?? null;
let _value = initialValue;
let _previousParams;
const _signal = $signal(loadingState(_value));
// Optimistic updates are enabled by default
const _optimisticMutate = options?.optimisticMutate ?? true;
const _fetchWhen = options?.fetchWhen ?? (() => true);
const execute = async () => {
const derivedSourceFn = source instanceof Function ? source : () => source;
try {
let data = null;
if (_fetchWhen()) {
if (fetchWhen()) {
const derivedSource = derivedSourceFn();
if (!_isInitialLoad && isEqual(derivedSource, _previousParams)) {
// No need to fetch the data again if the params haven't changed
Expand All @@ -289,7 +301,7 @@ function $resource(fn, source, options) {
error: null
};
} catch (error) {
_signal.value = {
_signal.value = onError(error, _value, { identifier, initialValue }) ?? {
data: null,
loading: false,
error
Expand All @@ -298,7 +310,9 @@ function $resource(fn, source, options) {
_isInitialLoad = false;
}
};
$effect(execute);
$effect(execute, {
identifier
});
/**
* Callback function that updates the value of the resource.
* @param value The value we want to set the resource to.
Expand All @@ -316,7 +330,7 @@ function $resource(fn, source, options) {
data: _signal.readOnly,
mutate: (newValue) => {
const previousValue = _value;
if (_optimisticMutate) {
if (optimisticMutate) {
// If optimistic updates are enabled, update the value immediately
mutatorCallback(newValue);
}
Expand All @@ -327,7 +341,8 @@ function $resource(fn, source, options) {
refetch: async () => {
_isInitialLoad = true;
await execute();
}
},
identifier
};
}
function isSignal(anything) {
Expand Down
Loading

0 comments on commit f4f7351

Please sign in to comment.