Initial commit
This commit is contained in:
427
references/top-errors.md
Normal file
427
references/top-errors.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# Top 15+ Documented Errors and Solutions
|
||||
|
||||
Complete reference for common Durable Objects errors and how to prevent them.
|
||||
|
||||
---
|
||||
|
||||
## 1. Class Not Exported
|
||||
|
||||
**Error:** `"binding not found"`, `"Class X not found"`
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/get-started/
|
||||
|
||||
**Why It Happens:** Durable Object class not exported from Worker
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
export class MyDO extends DurableObject { }
|
||||
|
||||
// CRITICAL: Export as default
|
||||
export default MyDO;
|
||||
|
||||
// In Worker, also export for Wrangler
|
||||
export { MyDO };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Missing Migration
|
||||
|
||||
**Error:** `"migrations required"`, `"no migration found for class"`
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
|
||||
|
||||
**Why It Happens:** Created DO class without migration entry
|
||||
|
||||
**Solution:** Always add migration when creating new DO class
|
||||
```jsonc
|
||||
{
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["MyDO"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Wrong Migration Type (KV vs SQLite)
|
||||
|
||||
**Error:** Schema errors, storage API mismatch
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
|
||||
|
||||
**Why It Happens:** Used `new_classes` instead of `new_sqlite_classes`
|
||||
|
||||
**Solution:** Use `new_sqlite_classes` for SQLite backend (recommended)
|
||||
```jsonc
|
||||
{
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["MyDO"] // ← SQLite (1GB, atomic)
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Constructor Overhead Blocks Hibernation Wake
|
||||
|
||||
**Error:** Slow hibernation wake-up times, high latency
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/
|
||||
|
||||
**Why It Happens:** Heavy work in constructor delays all requests
|
||||
|
||||
**Solution:** Minimize constructor, use `blockConcurrencyWhile()`
|
||||
```typescript
|
||||
constructor(ctx, env) {
|
||||
super(ctx, env);
|
||||
|
||||
// Minimal initialization
|
||||
this.sessions = new Map();
|
||||
|
||||
// Load from storage (blocks requests until complete)
|
||||
ctx.blockConcurrencyWhile(async () => {
|
||||
this.data = await ctx.storage.get('data');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. setTimeout Breaks Hibernation
|
||||
|
||||
**Error:** DO never hibernates, high duration charges
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/
|
||||
|
||||
**Why It Happens:** `setTimeout`/`setInterval` prevents hibernation
|
||||
|
||||
**Solution:** Use alarms API instead
|
||||
```typescript
|
||||
// ❌ WRONG: Prevents hibernation
|
||||
setTimeout(() => this.doWork(), 60000);
|
||||
|
||||
// ✅ CORRECT: Allows hibernation
|
||||
await this.ctx.storage.setAlarm(Date.now() + 60000);
|
||||
|
||||
async alarm() {
|
||||
this.doWork();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. In-Memory State Lost on Hibernation
|
||||
|
||||
**Error:** WebSocket metadata lost, state reset unexpectedly
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/best-practices/websockets/
|
||||
|
||||
**Why It Happens:** Relied on in-memory state that's cleared on hibernation
|
||||
|
||||
**Solution:** Use `serializeAttachment()` for WebSocket metadata
|
||||
```typescript
|
||||
// Persist metadata
|
||||
ws.serializeAttachment({ userId, username });
|
||||
|
||||
// Restore in constructor
|
||||
constructor(ctx, env) {
|
||||
super(ctx, env);
|
||||
|
||||
this.sessions = new Map();
|
||||
|
||||
ctx.getWebSockets().forEach(ws => {
|
||||
const metadata = ws.deserializeAttachment();
|
||||
this.sessions.set(ws, metadata);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Outgoing WebSocket Cannot Hibernate
|
||||
|
||||
**Error:** High charges despite using hibernation API
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/best-practices/websockets/
|
||||
|
||||
**Why It Happens:** Outgoing WebSockets don't support hibernation
|
||||
|
||||
**Solution:** Only use hibernation for server-side (incoming) WebSockets
|
||||
|
||||
**Note:** DO must be WebSocket server, not client.
|
||||
|
||||
---
|
||||
|
||||
## 8. Global Uniqueness Confusion
|
||||
|
||||
**Error:** Unexpected DO class name conflicts across Workers
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness
|
||||
|
||||
**Why It Happens:** DO class names are globally unique per account
|
||||
|
||||
**Solution:** Understand scope and use unique class names
|
||||
```typescript
|
||||
// Worker A
|
||||
export class CounterA extends DurableObject { }
|
||||
|
||||
// Worker B
|
||||
export class CounterB extends DurableObject { }
|
||||
|
||||
// ❌ WRONG: Both use "Counter" → conflict
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Partial deleteAll on KV Backend
|
||||
|
||||
**Error:** Storage not fully deleted, billing continues
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/
|
||||
|
||||
**Why It Happens:** KV backend `deleteAll()` can fail partially
|
||||
|
||||
**Solution:** Use SQLite backend for atomic deleteAll
|
||||
```jsonc
|
||||
{
|
||||
"migrations": [
|
||||
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] } // Atomic operations
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Binding Name Mismatch
|
||||
|
||||
**Error:** Runtime error accessing DO binding, `undefined`
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/get-started/
|
||||
|
||||
**Why It Happens:** Binding name in wrangler.jsonc doesn't match code
|
||||
|
||||
**Solution:** Ensure consistency
|
||||
```jsonc
|
||||
{
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{ "name": "MY_DO", "class_name": "MyDO" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Must match binding name
|
||||
env.MY_DO.getByName('instance');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. State Size Exceeded
|
||||
|
||||
**Error:** `"state limit exceeded"`, storage errors
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/platform/pricing/
|
||||
|
||||
**Why It Happens:** Exceeded 1GB (SQLite) or 128MB (KV) limit
|
||||
|
||||
**Solution:** Monitor storage size, implement cleanup
|
||||
```typescript
|
||||
async checkStorageSize(): Promise<void> {
|
||||
const size = await this.estimateSize();
|
||||
|
||||
if (size > 900_000_000) { // 900MB
|
||||
await this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async alarm() {
|
||||
// Periodic cleanup
|
||||
const cutoff = Date.now() - (30 * 24 * 60 * 60 * 1000);
|
||||
this.sql.exec('DELETE FROM messages WHERE created_at < ?', cutoff);
|
||||
|
||||
await this.ctx.storage.setAlarm(Date.now() + 86400000);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Migration Not Atomic
|
||||
|
||||
**Error:** Gradual deployment blocked, migration errors
|
||||
|
||||
**Source:** https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/
|
||||
|
||||
**Why It Happens:** Tried to use gradual rollout with migrations
|
||||
|
||||
**Solution:** Understand migrations deploy atomically
|
||||
- All DO instances migrate at once
|
||||
- Cannot use gradual deployment with migrations
|
||||
- Test thoroughly before deploying
|
||||
|
||||
---
|
||||
|
||||
## 13. Location Hint Ignored
|
||||
|
||||
**Error:** DO created in wrong region, higher latency
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/reference/data-location/
|
||||
|
||||
**Why It Happens:** Location hints are best-effort, not guaranteed
|
||||
|
||||
**Solution:** Use jurisdiction for strict requirements
|
||||
```typescript
|
||||
// ⚠️ Best-effort (not guaranteed)
|
||||
const stub = env.MY_DO.get(id, { locationHint: 'enam' });
|
||||
|
||||
// ✅ Strictly enforced
|
||||
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });
|
||||
const stub = env.MY_DO.get(euId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Alarm Retry Failures
|
||||
|
||||
**Error:** Tasks lost after repeated alarm failures
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/api/alarms/
|
||||
|
||||
**Why It Happens:** Alarm handler throws errors repeatedly, exhausts retries
|
||||
|
||||
**Solution:** Implement idempotent alarm handlers with retry limits
|
||||
```typescript
|
||||
async alarm(info: { retryCount: number }): Promise<void> {
|
||||
if (info.retryCount > 3) {
|
||||
console.error('Giving up after 3 retries');
|
||||
// Log failure, clean up state
|
||||
await this.logFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
// Idempotent operation (safe to retry)
|
||||
await this.processWithIdempotency();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Fetch Blocks Hibernation
|
||||
|
||||
**Error:** DO never hibernates despite using hibernation API
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/
|
||||
|
||||
**Why It Happens:** In-progress `fetch()` requests prevent hibernation
|
||||
|
||||
**Solution:** Ensure all async I/O completes before idle period
|
||||
```typescript
|
||||
async webSocketMessage(ws: WebSocket, message: string): Promise<void> {
|
||||
// ✅ GOOD: Await all I/O before returning
|
||||
const response = await fetch('https://api.example.com/data');
|
||||
const data = await response.json();
|
||||
ws.send(JSON.stringify(data));
|
||||
// Handler completes → can hibernate
|
||||
|
||||
// ❌ BAD: Background fetch prevents hibernation
|
||||
this.ctx.waitUntil(
|
||||
fetch('https://api.example.com/log').then(r => r.json())
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Cannot Enable SQLite on Existing KV DO
|
||||
|
||||
**Error:** Migration fails, schema errors
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
|
||||
|
||||
**Why It Happens:** Attempted to migrate existing KV-backed DO to SQLite
|
||||
|
||||
**Solution:** Create new SQLite-backed DO class, migrate data manually
|
||||
```jsonc
|
||||
// ❌ WRONG: Cannot change existing DO backend
|
||||
{
|
||||
"migrations": [
|
||||
{ "tag": "v1", "new_classes": ["Counter"] }, // KV backend
|
||||
{ "tag": "v2", "renamed_classes": [{ "from": "Counter", "to": "CounterSQLite" }] }
|
||||
// This doesn't change backend!
|
||||
]
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Create new class
|
||||
{
|
||||
"migrations": [
|
||||
{ "tag": "v1", "new_classes": ["Counter"] },
|
||||
{ "tag": "v2", "new_sqlite_classes": ["CounterV2"] }
|
||||
]
|
||||
}
|
||||
|
||||
// Then migrate data from Counter to CounterV2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. SQL Injection Vulnerability
|
||||
|
||||
**Error:** Security vulnerability, data breach
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
|
||||
|
||||
**Why It Happens:** String concatenation in SQL queries
|
||||
|
||||
**Solution:** Always use parameterized queries
|
||||
```typescript
|
||||
// ❌ WRONG: SQL injection risk
|
||||
this.sql.exec(`SELECT * FROM users WHERE email = '${userEmail}'`);
|
||||
|
||||
// ✅ CORRECT: Parameterized query
|
||||
this.sql.exec('SELECT * FROM users WHERE email = ?', userEmail);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18. Standard WebSocket API Used
|
||||
|
||||
**Error:** High duration charges, no hibernation
|
||||
|
||||
**Source:** https://developers.cloudflare.com/durable-objects/best-practices/websockets/
|
||||
|
||||
**Why It Happens:** Used `ws.accept()` instead of `ctx.acceptWebSocket()`
|
||||
|
||||
**Solution:** Use hibernation API
|
||||
```typescript
|
||||
// ❌ WRONG: Standard API, no hibernation
|
||||
server.accept();
|
||||
|
||||
// ✅ CORRECT: Hibernation API
|
||||
this.ctx.acceptWebSocket(server);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Error Lookup
|
||||
|
||||
| Error Message | Issue # | Quick Fix |
|
||||
|---------------|---------|-----------|
|
||||
| "binding not found" | #1 | Export DO class |
|
||||
| "migrations required" | #2 | Add migration |
|
||||
| Slow wake-up | #4 | Minimize constructor |
|
||||
| High duration charges | #5, #15 | Use alarms, await I/O |
|
||||
| State lost | #6 | serializeAttachment |
|
||||
| "state limit exceeded" | #11 | Implement cleanup |
|
||||
| "SQL injection" | #17 | Parameterized queries |
|
||||
|
||||
---
|
||||
|
||||
**For more help:** Check official docs and GitHub issues at https://github.com/cloudflare/workerd/issues
|
||||
Reference in New Issue
Block a user