| name | elysia |
| description | Elysia.js: error handling, status responses, plugin composition. Use for Elysia, Eden Treaty, API route handlers, HTTP errors, type-safe clients. |
| metadata | {"author":"epicenter","version":"1.0"} |
Elysia.js Patterns (v1.2+)
Reference Repositories
- Hono : Ultrafast web framework for Cloudflare Workers
- Cloudflare Docs : Cloudflare Workers, Durable Objects, KV documentation
Upstream Grounding
When Worker runtime behavior, Hono middleware semantics, request/response streaming, Durable Objects, WebSockets, or Cloudflare deployment limits affect correctness, ask DeepWiki a narrow question against honojs/hono or cloudflare/cloudflare-docs before relying on memory. Use it to orient, then verify decisive details against local installed types, source, or official docs before changing code.
Skip DeepWiki for Elysia-specific conventions already documented below.
When to Apply This Skill
Use this pattern when you need to:
- Write or refactor Elysia handlers to use
status() responses.
- Define per-status response schemas for Eden Treaty type safety.
- Migrate handlers away from
set.status plus error-object returns.
- Compose Elysia plugins/guards for shared auth and route behavior.
- Choose between
return status(...) and throw status(...) by control-flow context.
The status() Helper (ALWAYS use this)
Never use set.status + return object. Always destructure status from the handler context and use it for all non-200 responses. This gives you:
- Typesafe string literals with full IntelliSense (e.g.
"Bad Request" instead of 400)
- Automatic response type inference per status code
- Eden Treaty end-to-end type safety on error responses
Basic Usage
import { Elysia, t } from 'elysia';
new Elysia().post(
'/chat',
async ({ body, headers, status }) => {
if (!isValid(body.provider)) {
return status('Bad Request', 'Unsupported provider');
}
if (!apiKey) {
return status('Unauthorized', 'Missing API key');
}
return doWork(body);
},
{
response: {
200: t.Any(),
400: t.String(),
401: t.String(),
},
},
);
return status() vs throw status()
Both work. The framework handles either. The difference is purely control flow:
| Pattern | Behavior | Use when |
|---|
return status(...) | Normal return, continues to response pipeline | You're at a natural return point (validation guards, end of handler) |
throw status(...) | Short-circuits execution immediately | You're deep in nested logic or inside a try/catch and want to bail out |
This codebase convention: prefer return status(...). It matches the existing early-return-on-error pattern used everywhere else (see error-handling skill). Reserve throw status(...) for catch blocks or deeply nested code where return would be awkward.
async ({ body, status }) => {
if (!isValid(body.provider)) {
return status('Bad Request', `Unsupported provider: ${body.provider}`);
}
const apiKey = resolveApiKey(body.provider, headerApiKey);
if (!apiKey) {
return status('Unauthorized', 'Missing API key');
}
return doWork(body);
};
async ({ body, status }) => {
try {
return await streamResponse(body);
} catch (error) {
if (isAbortError(error)) {
throw status(499, 'Client closed request');
}
throw status('Bad Gateway', `Provider error: ${error.message}`);
}
};
Type inference is identical for both
Both return status(...) and throw status(...) produce the same ElysiaCustomStatusResponse object. Elysia's type system infers response types from the response schema in route options, not from how you invoke status(). Eden Treaty type safety works equally with either approach.
Available String Status Codes (StatusMap)
Use these string literals instead of numeric codes for better readability:
| String Literal | Code | Common Use |
|---|
'Bad Request' | 400 | Validation failures, malformed input |
'Unauthorized' | 401 | Missing/invalid auth credentials |
'Forbidden' | 403 | Valid auth but insufficient permissions |
'Not Found' | 404 | Resource doesn't exist |
'Conflict' | 409 | State conflict (duplicate, already exists) |
'Unprocessable Content' | 422 | Semantically invalid input |
'Too Many Requests' | 429 | Rate limiting |
'Internal Server Error' | 500 | Unexpected server failure |
'Bad Gateway' | 502 | Upstream provider error |
'Service Unavailable' | 503 | Temporary overload/maintenance |
For non-standard codes (e.g. nginx's 499), use the numeric literal directly: status(499, 'Client closed request').
Response Schemas for Eden Treaty Type Safety
Define response schemas per status code in route options. This is what makes Eden Treaty infer error types on the client:
new Elysia().post(
'/chat',
async ({ body, status }) => {
if (!isValid(body.provider)) {
return status('Bad Request', `Unsupported provider: ${body.provider}`);
}
return streamResult;
},
{
body: t.Object({
provider: t.String(),
model: t.String(),
}),
response: {
200: t.Any(),
400: t.String(),
401: t.String(),
502: t.String(),
},
},
);
Eden Treaty then infers:
const { data, error } = await api.chat.post({
provider: 'openai',
model: 'gpt-4',
});
if (error) {
switch (error.status) {
case 400:
case 401:
case 502:
}
}
Error Response Body: Strings vs Objects
Prefer plain strings as error bodies. The status code already communicates the error class. A descriptive string message is sufficient and keeps the API simple.
return status('Bad Request', `Unsupported provider: ${provider}`);
return status('Unauthorized', 'Missing API key: set x-provider-api-key header');
set.status = 400;
return { error: `Unsupported provider: ${provider}` };
If you need structured error bodies (multiple fields, error codes, validation details), define a TypeBox schema:
const ErrorBody = t.Object({
message: t.String(),
code: t.Optional(t.String()),
});
response: {
400: ErrorBody,
401: ErrorBody,
}
Plugin Composition
Elysia plugins are just functions that return Elysia instances. Use new Elysia() inside the plugin, not new Elysia({ prefix }) : let the consumer control mounting:
export function createMyPlugin() {
return new Elysia().post('/endpoint', async ({ body, status }) => {
});
}
app.use(new Elysia({ prefix: '/api' }).use(createMyPlugin()));
Guards for Shared Auth
Use .guard() with beforeHandle for auth that applies to multiple routes:
const authed = new Elysia().guard({
async beforeHandle({ headers, status }) {
const token = extractBearerToken(headers.authorization);
if (!isValid(token)) {
return status('Unauthorized', 'Invalid or missing token');
}
},
});
return authed
.get('/protected', () => 'secret')
.post('/admin', () => 'admin stuff');
Migration Checklist: set.status to status()
When updating existing handlers:
- Replace
set with status in the handler destructuring
- Replace
set.status = N; return { error: msg }; with return status('String Literal', msg);
- In catch blocks, use
throw status(...) instead of set.status = N; return { error: msg };
- Add
response schemas to route options for Eden Treaty type inference
- Keep
set in the destructuring ONLY if you still need set.headers for things like content-type
async ({ body, headers, set }) => {
if (!valid) {
set.status = 400;
return { error: 'Bad input' };
}
};
async ({ body, headers, status }) => {
if (!valid) {
return status('Bad Request', 'Bad input');
}
};
async ({ body, headers, set, status }) => {
if (!valid) {
return status('Bad Request', 'Bad input');
}
set.headers['content-type'] = 'application/octet-stream';
return binaryData;
};