Spoosh
Guides

Error Handling

Handle errors at every level of your application

Spoosh provides multiple layers for handling errors: per-inject, per-trigger, global plugins, and typed errors.

Basic Error Handling

All Spoosh injects return an error signal:

@Component({
  selector: "app-user-profile",
  template: `
    @if (loading()) {
      <div>Loading...</div>
    } @else if (error()) {
      <div>Failed to load profile: {{ error()?.message }}</div>
    } @else {
      <div>{{ data()?.name }}</div>
    }
  `,
})
export class UserProfileComponent {
  private user = injectRead((api) =>
    api("users/:id").GET({ params: { id: "1" } })
  );

  data = this.user.data;
  loading = this.user.loading;
  error = this.user.error;
}

Typed Errors

Define a global error type through the second generic parameter on Spoosh. The type flows through to all injects automatically:

interface ApiError {
  message: string;
  code: string;
  details?: Record<string, string[]>;
}

const spoosh = new Spoosh<ApiSchema, ApiError>("/api").use([...]);

Per-Route Error Types

Override the global error type for specific routes by adding an error field to your schema. Routes without error fall back to the global type:

import { SpooshSchema } from "@spoosh/core";

type ApiSchema = SpooshSchema<{
  "users/:id": {
    GET: {
      data: User;
      // No error field — uses global ApiError
    };
  };
  "auth/login": {
    POST: {
      body: { email: string; password: string };
      data: { token: string };
      error: LoginError; // Overrides global ApiError
    };
  };
}>;

interface LoginError {
  message: string;
  code: "INVALID_CREDENTIALS" | "ACCOUNT_LOCKED" | "EMAIL_NOT_VERIFIED";
  remainingAttempts?: number;
}

Per-Request Error Handling

The trigger function returns a promise with both data and error, letting you handle errors inline with full type inference:

@Component({
  selector: "app-login-form",
  template: `<form (ngSubmit)="handleSubmit()">...</form>`,
})
export class LoginFormComponent {
  private loginWriter = injectWrite((api) => api("auth/login").POST());

  async handleSubmit() {
    const { data, error } = await this.loginWriter.trigger({
      body: this.credentials,
    });

    if (error) {
      // error is typed as LoginError (from schema)
      if (error.code === "ACCOUNT_LOCKED") {
        this.showLockedMessage();
        return;
      }
      this.showToast(error.message);
      return;
    }

    this.saveToken(data.token);
    this.router.navigate(["/dashboard"]);
  }
}

Global Error Handling

Show a toast for all errors globally using a custom plugin:

import { SpooshPlugin } from "@spoosh/core";

function globalErrorPlugin(): SpooshPlugin {
  return {
    name: "global-error",
    operations: ["read", "write", "pages"],
    afterResponse: (context, response) => {
      const error = response.error as ApiError | undefined;

      if (error) {
        showToast(`Request failed: ${error.message}`);
      }
    },
  };
}
const spoosh = new Spoosh<ApiSchema, Error>("/api").use([
  globalErrorPlugin(),
  cachePlugin({ staleTime: 5000 }),
  invalidationPlugin(),
]);

On this page