Elysia 1.0 – Lament of the Fallen
Published 16 Mar 2024 by @saltyaom

Overview
Elysia 1.0 is the first stable release after 1.8 years of development.
It brings a suite of performance improvements and a handful of breaking changes:
- Sucrose – rewritten pattern‑matching static analysis
- Improved startup time (up to 14×)
- Removed ~40 routes/instance TypeScript limitation
- Faster type inference (up to ~3.8×)
- Treaty 2 – ergonomic API overhaul
- Hook type – breaking change to hook inheritance
- Inline error – tighter type‑checking for error responses
“Lament of the Fallen” is a reference to a song from Honkai Impact 3rd.
⚡ Tip: ElysiaJS is open source and maintained by volunteers. It is not affiliated with Mihoyo or Hoyoverse, though the author loves the Honkai series.
Table of Contents
- Sucrose
- Improved Startup Time
- Remove ~40 routes/instance limitation
- Type Inference Improvement
- Treaty 2
- Hook type (breaking change)
- Inline error
- What does it mean for v1, and what's next
Sucrose
Elysia uses a custom JIT static code‑analysis engine.
The original implementation relied on complex RegEx and could be slow, especially with recursion.
Sucrose replaces that with a hybrid approach combining partial AST‑based analysis and pattern matching, resulting in:
- Up to 37 % faster inference time
- Lower memory usage
The new engine is activated automatically from Elysia 1.0 onward.
Improved Startup Time
By deferring the JIT analysis to the first request (lazy evaluation) rather than pre‑compiling all routes, the compile phase is cached per route.
Typical compilation time: 0.01 – 0.03 ms per route.
Stress test on a medium‑sized application shows startup improvements of 6.5‑14×.
Remove ~40 routes/instance limitation
Until Elysia 0.1, a single instance could only expose ~40 routes due to TypeScript’s “Type instantiation is excessively deep and possibly infinite” limit.
Example that fails:
const main = new Elysia()
.get('/1', () => '1')
.get('/2', () => '2')
.get('/3', () => '3')
// repeat for 40 times
.get('/42', () => '42');
// Type instantiation is excessively deep and possibly infinite
Work‑around – split into multiple controllers:
const controller1 = new Elysia()
.get('/42', () => '42')
// repeat for 40 times
const main = new Elysia()
.get('/1', () => '1')
.get('/2', () => '2')
.get('/3', () => '3')
// repeat for 40 times
.use(controller1);
Elysia 1.0 removes this limitation via Tail Call Optimization and other type‑performance optimisations.
You can now stack ≈ 558 routes per instance before hitting the JavaScript stack/queue limit.
const main = new Elysia()
.get('/1', () => '1')
.get('/2', () => '2')
.get('/3', () => '3')
// repeat for n times
.get('/550', () => '550');
Type Inference Improvement
With the removed stack limit and new optimisations, type‑check and autocompletion remain instant even with 500+ routes.
| Test | Elysia 0.8 | Elysia 1.0 |
|---|---|---|
| 450‑route startup | ~1500 ms | ~400 ms |

You can stack over 1 000 routes per single Eden Treaty instance.
Treaty 2
Treaty 2 is a redesign of the original Eden Treaty library to improve ergonomics and type safety:
- More ergonomic syntax
- End‑to‑end type safety for unit tests
- Interceptors
- No
$prefix/property
Example:
import { describe, expect, it } from 'bun:test';
import { Elysia } from 'elysia';
import { treaty } from '@elysiajs/eden';
const app = new Elysia().get('/hello', () => 'hi');
const api = treaty(app);
describe('Elysia', () => {
it('return a response', async () => {
const { data } = await api.hello.get();
expect(data).toBe('hi');
});
});
Migrating from Treaty 1 is straightforward. Import treaty for Treaty 2 and edenTreaty for the legacy API.
See the full docs:
Hook type (breaking change)
Previously, hooks like onTransform or onBeforeHandle were global by default.
This caused side‑effects and nested guard boilerplate.
Old behaviour
const plugin = new Elysia()
.onBeforeHandle(() => console.log('Hi'))
.get('/hi', () => 'in plugin');
const app = new Elysia()
.use(plugin)
.get('/no-hi-please', () => 'oh no');
New behaviour with hook types
Hook types classify inheritance:
| Type | child | current | parent | main |
|---|---|---|---|---|
local | ✅ | ✅ | ❌ | ❌ |
scope | ✅ | ✅ | ✅ | ❌ |
global | ✅ | ✅ | ✅ | ✅ |
const plugin = new Elysia()
.onBeforeHandle({ as: 'global' }, () => console.log('Hi'))
.get('/child', () => 'log hi');
const child = new Elysia().get('/child', () => 'hello');
const current = new Elysia()
.onBeforeHandle({ as: type }, () => console.log('hi'))
.use(child)
.get('/current', () => 'hello');
const parent = new Elysia().use(current).get('/parent', () => 'hello');
const main = new Elysia().use(parent).get('/main', () => 'hello');
Migration notes are available on the GitHub issue:
https://github.com/elysiajs/elysia/issues/513
Inline error
From Elysia 0.8 onward you can use the error function to return custom status codes.
However, specifying a response schema prevented fine‑grained autocompletion.
Inline error solves this by destructuring error from the handler:
import { Elysia } from 'elysia';
new Elysia()
.get(
'/hello',
({ error }) => {
if (Math.random() > 0.5) return error(418, 'Nagisa');
return 'Azusa';
},
{
response: t.Object({
200: t.Literal('Azusa'),
418: t.Literal('Nagisa'),
}),
},
);
The resulting type narrowing provides autocompletion for the exact status code.

What does it mean for v1, and what's next
Reaching a stable release means Elysia is production‑ready.
Future focus areas:
- Ecosystem & plugin refinement
- Unified runtime support (Node, Deno, Cloudflare Workers, Vercel Edge, Netlify Functions, AWS Lambda/LLRT)
- Bun‑specific features (e.g., loaders API)
- WinterCG compatibility – not all plugins yet
- Server‑side rendering and edge function support for Next.js, Expo, Astro, SvelteKit
Quote
“Bun was right, the best way to migrate people from Node is to have a compatibility layer and offer better DX and performance on Bun.”
— SaltyAom (Twitter)
More on the ecosystem and polyfills can be found on the GitHub repositories and documentation pages.