Results All the Way Up
Let errors travel through the application until the event handler turns them into something the user can see.
Pass Results all the way up, then handle the outcome at the event handler and return void.
<button
onclick={async (): Promise<void> => {
const result: Result<void, AppError> = await completeOperation();
if (result.error) toast.error(result.error.message);
}}
>
Do the thing
</button>
Each caller can handle that result or pass it back up. But the event handler doesn’t return another
Result. It decides what to show the user.
The result keeps moving upward
event handler -> toast, dialog, or done
^
| Result
complete operation
^
| Result
smaller operation
Suppose one operation produces data that another operation needs:
const first: Result<Data, AppError> = await operationOne();
if (first.error) return Err(first.error);
const second: Result<void, AppError> = await operationTwo(first.data);
The lower functions do not decide how an error should appear to a person. They return the error so the next layer can decide whether it can recover, add more context, or pass it upward again.
The event handler is the boundary
The complete operation composes the smaller operations. The button handles its final result:
<script lang="ts">
import { Err, Ok, type Result } from 'wellcrafted/result';
import { toast } from 'svelte-sonner';
// Returns the outcome to its caller.
async function completeOperation(): Promise<Result<void, AppError>> {
const { data, error } = await operationOne();
if (error) return Err(error);
const { error: secondError } = await operationTwo(data);
if (secondError) return Err(secondError);
return Ok(undefined);
}
</script>
<button
type="button"
onclick={async (): Promise<void> => {
const { error } = await completeOperation();
// Handles the outcome. No Result goes back to the caller.
if (error) toast.error(error.message);
}}
>
Do the thing
</button>
completeOperation returns a result whose success value is void. It still tells its caller whether the operation succeeded. The event handler returns Promise<void>: it has handled the outcome, so there is no result left to pass back.
Here, we’ve decided that an error should appear as a toast. The operation supplies the error; the event handler chooses how to show it.
Extracting the pattern
If several buttons handle errors this way, we can pull that behavior into a toastOnError helper:
<script lang="ts">
import { toast } from 'svelte-sonner';
function toastOnError(result: Result<unknown, AppError>): void {
if (result.error !== null) toast.error(result.error.message);
}
</script>
<button
type="button"
onclick={async () => toastOnError(await completeOperation())}
>
Do the thing
</button>
The architecture is now compressed into a single expression:
completeOperation() → Result
→ toastOnError(...)
→ user-facing effect
Results all the way up. At the event handler, the result becomes a response to the user.