Error Handling

When building functions in Keel, you'll often need to communicate specific error conditions back to API consumers. The @teamkeel/sdk package provides a set of built-in error types that map to standard HTTP status codes and error responses.

Built-in Error Types

Keel provides three error types that you can throw from your functions:

import { errors } from "@teamkeel/sdk";
Error TypeHTTP StatusError CodeDescription
errors.NotFound404ERR_RECORD_NOT_FOUNDThe requested resource could not be found
errors.BadRequest400ERR_INVALID_INPUTThe request is invalid or malformed
errors.Unknown500ERR_UNKNOWNAn unexpected error occurred

Throwing Errors in Functions

You can throw errors from any function, whether it's a custom function, action hook, job, or subscriber. To throw an error, use the throw keyword with new and the appropriate error class.

Basic Usage

functions/getProduct.ts
import { GetProduct, errors } from "@teamkeel/sdk";
 
const hooks: GetProductHooks = {
  afterQuery(ctx, inputs, product) {
    if (!product) {
      throw new errors.NotFound();
    }
 
    if (!product.isActive) {
      throw new errors.BadRequest("Product is no longer available");
    }
 
    return product;
  },
};
 
export default GetProduct(hooks);

Custom Error Messages

All error types accept an optional message parameter that will be included in the API response:

// With default message
throw new errors.NotFound();
 
// With custom message
throw new errors.NotFound("No order found with that reference");
 
// BadRequest with validation context
throw new errors.BadRequest("Quantity must be greater than zero");
 
// Unknown for unexpected conditions
throw new errors.Unknown("Failed to process payment");

Error Responses to Clients

When you throw an error, Keel transforms it into a structured JSON response. The format depends on which API protocol you're using.

JSON-RPC Response

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": "ERR_RECORD_NOT_FOUND",
    "message": "record not found"
  }
}

GraphQL Response

{
  "data": null,
  "errors": [
    {
      "message": "record not found",
      "extensions": {
        "code": "ERR_RECORD_NOT_FOUND"
      }
    }
  ]
}

Validation Errors

For input validation, use errors.BadRequest. This signals to the client that their request was malformed or contained invalid data.

functions/createOrder.ts
import { CreateOrder, errors } from "@teamkeel/sdk";
 
const hooks: CreateOrderHooks = {
  beforeWrite(ctx, inputs, values) {
    // Validate quantity
    if (values.quantity <= 0) {
      throw new errors.BadRequest("Quantity must be a positive number");
    }
 
    // Validate date
    if (values.deliveryDate < new Date()) {
      throw new errors.BadRequest("Delivery date cannot be in the past");
    }
 
    // Validate business rules
    if (values.quantity > 1000 && !ctx.isAuthenticated) {
      throw new errors.BadRequest("Large orders require authentication");
    }
 
    return values;
  },
};
 
export default CreateOrder(hooks);

Keel automatically validates inputs against your schema before your function runs. Use errors.BadRequest for business logic validation that goes beyond schema constraints.

Permission Errors

For access control, use the permissions API rather than throwing errors directly. When you call permissions.deny(), Keel throws a permission error internally and returns an ERR_PERMISSION_DENIED response.

functions/deleteOrder.ts
import { DeleteOrder, permissions } from "@teamkeel/sdk";
 
const hooks: DeleteOrderHooks = {
  beforeWrite(ctx, inputs, record) {
    // Only allow deletion of draft orders
    if (record.status !== "Draft") {
      permissions.deny();
    }
 
    // Only the creator can delete
    if (record.createdById !== ctx.identity?.id) {
      permissions.deny();
    }
  },
};
 
export default DeleteOrder(hooks);
⚠️

Calling permissions.deny() immediately stops execution and returns an error. Any code after this call will not run.

For custom read and write functions, you must explicitly call permissions.allow() for the request to succeed:

functions/processPayment.ts
import { ProcessPayment, permissions } from "@teamkeel/sdk";
 
export default ProcessPayment(async (ctx, inputs) => {
  // Validate the API key
  const apiKey = ctx.headers.get("X-API-Key");
  if (apiKey !== ctx.secrets.PAYMENT_API_KEY) {
    permissions.deny();
  }
 
  // Must explicitly allow
  permissions.allow();
 
  // Process the payment...
});

Automatic Database Errors

Keel automatically catches and transforms common database errors into appropriate API responses. You don't need to handle these yourself:

Database ErrorError CodeExample Cause
Unique constraint violationERR_INVALID_INPUTInserting a duplicate value for a @unique field
Foreign key constraintERR_INVALID_INPUTReferencing a record that doesn't exist
Not null constraintERR_INVALID_INPUTMissing a required field value
Record not foundERR_RECORD_NOT_FOUNDUpdating or deleting a non-existent record

For example, if you try to create a product with a duplicate SKU:

{
  "error": {
    "code": "ERR_INVALID_INPUT",
    "message": "the value for the unique field 'sku' must be unique"
  }
}

Best Practices

Use the Right Error Type

Choose the error type that best describes the problem:

// Resource doesn't exist → NotFound
if (!order) {
  throw new errors.NotFound("Order not found");
}
 
// Invalid input or business rule violation → BadRequest
if (order.status === "Shipped") {
  throw new errors.BadRequest("Cannot modify a shipped order");
}
 
// Unexpected system error → Unknown
if (!paymentResult.success) {
  throw new errors.Unknown("Payment processing failed");
}

Provide Helpful Messages

Error messages should help API consumers understand what went wrong and how to fix it:

// ❌ Vague
throw new errors.BadRequest("Invalid input");
 
// ✅ Specific
throw new errors.BadRequest("Email address is already registered");
 
// ❌ Technical jargon
throw new errors.NotFound("FK constraint violation on order_id");
 
// ✅ User-friendly
throw new errors.NotFound("The referenced order does not exist");

Validate Early

Check for error conditions at the start of your function to fail fast:

functions/updateInventory.ts
import { UpdateInventory, errors, models } from "@teamkeel/sdk";
 
const hooks: UpdateInventoryHooks = {
  async beforeWrite(ctx, inputs, values, record) {
    // Validate early
    if (values.quantity < 0) {
      throw new errors.BadRequest("Quantity cannot be negative");
    }
 
    // Check business rules
    const location = await models.location.findOne({ id: record.locationId });
    if (!location?.isActive) {
      throw new errors.BadRequest("Cannot update inventory for inactive location");
    }
 
    // Proceed with the update
    return values;
  },
};
 
export default UpdateInventory(hooks);

Don't Catch and Rethrow

Let Keel handle error transformation. Don't wrap SDK errors in try-catch blocks unless you need to add context:

// ❌ Unnecessary
try {
  const product = await models.product.findOne({ id: inputs.productId });
  if (!product) {
    throw new errors.NotFound();
  }
} catch (e) {
  throw e; // Just rethrows the same error
}
 
// ✅ Direct
const product = await models.product.findOne({ id: inputs.productId });
if (!product) {
  throw new errors.NotFound("Product not found");
}

Use Transactions for Complex Operations

For operations that should succeed or fail together, enable database transactions. If an error is thrown, all changes are automatically rolled back:

functions/transferStock.ts
import { TransferStock, errors, models } from "@teamkeel/sdk";
 
const fn = async (ctx, inputs) => {
  // Reduce stock at source
  const source = await models.inventory.update(
    { id: inputs.sourceId },
    { quantity: { decrement: inputs.quantity } }
  );
 
  if (source.quantity < 0) {
    // This will roll back the update above
    throw new errors.BadRequest("Insufficient stock at source location");
  }
 
  // Increase stock at destination
  await models.inventory.update(
    { id: inputs.destinationId },
    { quantity: { increment: inputs.quantity } }
  );
};
 
// Enable transaction support
fn.config = { dbTransaction: true };
 
export default TransferStock(fn);