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 Type | HTTP Status | Error Code | Description |
|---|---|---|---|
errors.NotFound | 404 | ERR_RECORD_NOT_FOUND | The requested resource could not be found |
errors.BadRequest | 400 | ERR_INVALID_INPUT | The request is invalid or malformed |
errors.Unknown | 500 | ERR_UNKNOWN | An 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
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.
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.
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:
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 Error | Error Code | Example Cause |
|---|---|---|
| Unique constraint violation | ERR_INVALID_INPUT | Inserting a duplicate value for a @unique field |
| Foreign key constraint | ERR_INVALID_INPUT | Referencing a record that doesn't exist |
| Not null constraint | ERR_INVALID_INPUT | Missing a required field value |
| Record not found | ERR_RECORD_NOT_FOUND | Updating 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:
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:
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);