Skip to main content
Runner provides a structured approach to error handling with typed errors, error contracts, and error boundaries. Instead of throwing generic Error objects, define error types with metadata, HTTP codes, and remediation steps.

Why Typed Errors?

Type Safety

Catch specific error types with full TypeScript support

HTTP Integration

Map errors to HTTP status codes automatically

Remediation

Include user-facing solutions in error definitions

Observability

Structured error data for logs and monitoring

Basic Usage

Define errors with r.error() and use them anywhere:
import { r } from "@bluelibs/runner";

// Define a typed error
const InvalidCredentials = r
  .error<{ email: string }>("app.errors.InvalidCredentials")
  .httpCode(401)
  .remediation("Check that the email and password are correct.")
  .format((data) => `Invalid credentials for ${data.email}`)
  .build();

// Throw it
try {
  await login({ email: "[email protected]", password: "wrong" });
} catch (err) {
  InvalidCredentials.throw({ email: "[email protected]" });
}

// Catch it
try {
  await login(credentials);
} catch (err) {
  if (InvalidCredentials.is(err)) {
    console.log(err.data.email);    // "[email protected]"
    console.log(err.httpCode);      // 401
    console.log(err.remediation);   // "Check that..."
  }
}

RunnerError Class

All errors created with r.error() extend RunnerError:
export class RunnerError<TData = any> extends Error {
  id: string;           // Error identifier
  data: TData;          // Typed error data
  httpCode?: number;    // HTTP status code
  remediation?: string; // User-facing solution
  message: string;      // Formatted message
}

Defining Errors

Basic Error

const NotFound = r
  .error<{ resourceId: string }>("app.errors.NotFound")
  .build();

// Throw with data
NotFound.throw({ resourceId: "user-123" });

With HTTP Code

const Unauthorized = r
  .error<{ reason: string }>("app.errors.Unauthorized")
  .httpCode(403)
  .format((data) => `Unauthorized: ${data.reason}`)
  .build();

With Remediation

const QuotaExceeded = r
  .error<{ limit: number; current: number }>("app.errors.QuotaExceeded")
  .httpCode(429)
  .format((data) => `Quota exceeded: ${data.current}/${data.limit}`)
  .remediation((data) => 
    `You've reached your limit of ${data.limit}. Upgrade your plan to continue.`
  )
  .build();

With Validation Schema

import { z } from "zod";

const ValidationError = r
  .error<{ field: string; message: string }>("app.errors.ValidationError")
  .httpCode(400)
  .dataSchema(z.object({
    field: z.string(),
    message: z.string(),
  }))
  .format((data) => `Validation failed: ${data.field} - ${data.message}`)
  .build();

Error Methods

throw
function
Throw the error with data
throw(data: TData): never
NotFound.throw({ resourceId: "user-123" });
new
function
Create error instance without throwing
new(data: TData): RunnerError<TData>
const error = NotFound.new({ resourceId: "user-123" });
console.log(error.message);
is
function
Type guard to check if error matches
is(error: unknown): error is RunnerError<TData>
is(error: unknown, partialData: Partial<TData>): error is RunnerError<TData>
if (NotFound.is(err)) {
  console.log(err.data.resourceId);
}

// Check with partial data match
if (NotFound.is(err, { resourceId: "user-123" })) {
  console.log("Specific user not found");
}
optional
function
Mark error as optional dependency
optional(): { inner: IErrorHelper<TData>; [symbolOptionalDependency]: true }

Error Contracts

Declare which errors a task can throw:
const getUser = r
  .task("users.get")
  .dependencies({ db })
  .throws([NotFound, DatabaseError]) // ← Declare possible errors
  .run(async (input: { id: string }, { db }) => {
    const user = await db.users.findOne({ id: input.id });
    if (!user) {
      NotFound.throw({ resourceId: input.id });
    }
    return user;
  })
  .build();

// Query all errors a task can throw
const possibleErrors = getUser.throws; // [NotFound, DatabaseError]

Checking Error Types

Generic RunnerError Check

import { r } from "@bluelibs/runner";

try {
  await someOperation();
} catch (err) {
  if (r.error.is(err)) {
    console.log("Runner error:", err.id);
    console.log("Error data:", err.data);
    console.log("HTTP code:", err.httpCode);
  }
}

Specific Error Check

try {
  await getUser({ id: "123" });
} catch (err) {
  if (NotFound.is(err)) {
    console.log("User not found:", err.data.resourceId);
  } else if (DatabaseError.is(err)) {
    console.log("Database connection failed");
  } else {
    throw err; // Re-throw unknown errors
  }
}

Partial Data Matching

const ApiError = r
  .error<{ endpoint: string; statusCode: number }>("app.errors.ApiError")
  .build();

try {
  await callApi();
} catch (err) {
  // Check if error is ApiError with specific status code
  if (ApiError.is(err, { statusCode: 404 })) {
    console.log("Endpoint not found:", err.data.endpoint);
  } else if (ApiError.is(err, { statusCode: 500 })) {
    console.log("Server error");
  }
}

Error Boundaries

Catch unhandled errors at the process level:
import { run } from "@bluelibs/runner";

const { runTask, dispose } = await run(app, {
  errorBoundary: true, // Enable error boundary
  onUnhandledError: async ({ error, kind }) => {
    console.error(`Unhandled ${kind}:`, error);
    
    if (r.error.is(error)) {
      // Log structured error data
      await logger.error("Unhandled Runner error", {
        errorId: error.id,
        data: error.data,
        httpCode: error.httpCode,
      });
    }
    
    // Don't exit in development
    if (process.env.NODE_ENV !== "development") {
      process.exit(1);
    }
  },
});

Built-in Errors

Runner provides common error types:
import {
  contextError,
  platformUnsupportedFunctionError,
} from "@bluelibs/runner";

// Context not available
try {
  const ctx = requestContext.use();
} catch (err) {
  if (contextError.is(err)) {
    console.error("Context not available");
  }
}

// Platform feature not supported
try {
  const ctx = r.asyncContext("myContext").build();
} catch (err) {
  if (platformUnsupportedFunctionError.is(err)) {
    console.error("Async context not available on this platform");
  }
}

HTTP Status Codes

Define errors with standard HTTP codes:
const errors = {
  BadRequest: r.error("app.errors.BadRequest").httpCode(400).build(),
  Unauthorized: r.error("app.errors.Unauthorized").httpCode(401).build(),
  Forbidden: r.error("app.errors.Forbidden").httpCode(403).build(),
  NotFound: r.error("app.errors.NotFound").httpCode(404).build(),
  Conflict: r.error("app.errors.Conflict").httpCode(409).build(),
  TooManyRequests: r.error("app.errors.TooManyRequests").httpCode(429).build(),
  InternalError: r.error("app.errors.InternalError").httpCode(500).build(),
};
HTTP codes must be integers between 100 and 599.

Express Integration

import express from "express";
import { r } from "@bluelibs/runner";

const app = express();

// Error handler middleware
app.use((err: any, req: any, res: any, next: any) => {
  if (r.error.is(err)) {
    res.status(err.httpCode || 500).json({
      error: err.id,
      message: err.message,
      data: err.data,
      remediation: err.remediation,
    });
  } else {
    res.status(500).json({ error: "Internal Server Error" });
  }
});

// Route handler
app.get("/users/:id", async (req, res, next) => {
  try {
    const user = await runtime.runTask(getUser, { id: req.params.id });
    res.json(user);
  } catch (err) {
    next(err); // Express error middleware handles it
  }
});

Dynamic Remediation

const InsufficientFunds = r
  .error<{ required: number; available: number }>("app.errors.InsufficientFunds")
  .httpCode(402)
  .format((data) => 
    `Insufficient funds: need $${data.required}, have $${data.available}`
  )
  .remediation((data) => {
    const difference = data.required - data.available;
    return `Add $${difference} to your account to complete this transaction.`;
  })
  .build();

InsufficientFunds.throw({ required: 100, available: 50 });
// Message: "Insufficient funds: need $100, have $50"
// Remediation: "Add $50 to your account to complete this transaction."

Error Metadata

const DatabaseError = r
  .error<{ query: string; code?: string }>("app.errors.DatabaseError")
  .httpCode(500)
  .meta({
    description: "Database operation failed",
    category: "database",
    severity: "high",
  })
  .build();

console.log(DatabaseError.meta);
// { description: "...", category: "database", severity: "high" }

Best Practices

Use Semantic IDs

Name errors clearly: app.errors.InvalidCredentials, not error1

Include Context

Add relevant data to error objects for debugging

Provide Remediation

Tell users how to fix the problem, not just what went wrong

Use Error Contracts

Declare possible errors with .throws() for documentation

Common Patterns

Validation Errors

const ValidationError = r
  .error<{ field: string; rule: string; value: unknown }>("app.errors.ValidationError")
  .httpCode(400)
  .format((data) => `Validation failed: ${data.field} ${data.rule}`)
  .remediation((data) => `Check the value for ${data.field} and try again.`)
  .build();

if (input.email && !isValidEmail(input.email)) {
  ValidationError.throw({
    field: "email",
    rule: "must be a valid email address",
    value: input.email,
  });
}

Retry with Error Filtering

import { globals } from "@bluelibs/runner";

const callApi = r
  .task("api.call")
  .middleware([
    globals.middleware.task.retry.with({
      retries: 3,
      stopRetryIf: (error) => {
        // Don't retry 4xx errors
        if (r.error.is(error) && error.httpCode) {
          return error.httpCode >= 400 && error.httpCode < 500;
        }
        return false;
      },
    }),
  ])
  .run(async (url: string) => {
    const res = await fetch(url);
    if (!res.ok) {
      ApiError.throw({ statusCode: res.status, endpoint: url });
    }
    return res.json();
  })
  .build();

Error Wrapping

const ExternalApiError = r
  .error<{ originalError: string }>("app.errors.ExternalApiError")
  .httpCode(502)
  .build();

try {
  await thirdPartyApi.call();
} catch (err) {
  ExternalApiError.throw({
    originalError: err instanceof Error ? err.message : String(err),
  });
}

See Also

  • Tasks - Task execution and error handling
  • Middleware - Error handling in middleware
  • Testing - Testing error scenarios
  • Logging - Structured error logging

Build docs developers (and LLMs) love