Claude-skill-registry deno-patterns
Modern TypeScript patterns and migration guidance for Deno: resource management with 'using', async generators, error handling, and Web Standard APIs. Use when migrating from Node.js, implementing cleanup logic, or learning modern JS/TS patterns.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/deno-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-deno-patterns && rm -rf "$T"
skills/data/deno-patterns/SKILL.mdDeno Modern Patterns & Migration
When to Use This Skill
Use this skill when:
- Migrating from Node.js to Deno
- Learning modern JavaScript/TypeScript patterns
- Implementing resource management
- Working with timers, polling, or cleanup logic
- Handling errors in Deno
- Choosing between old and new patterns
Note: Always read
deno-core.md first for essential configuration and practices.
Resource Management with using
usingThe Problem with Manual Cleanup
Old Pattern:
// Manual timer cleanup - error-prone const id = setInterval(() => { console.log("Tick!"); }, 1000); // Later, cleanup (easy to forget!) clearInterval(id);
Problems:
- Resource leaks if cleanup is missed
- Not exception-safe
- Resource not tied to lexical scope
- Cleanup code separated from acquisition
The using
Statement (Deno >= v2.4)
usingNew Pattern:
// Automatic, exception-safe cleanup class Timer { #handle: number; constructor(cb: () => void, ms: number) { this.#handle = setInterval(cb, ms); } [Symbol.dispose]() { clearInterval(this.#handle); console.log("Timer disposed"); } } using timer = new Timer(() => { console.log("Tick!"); }, 1000); // Timer automatically cleaned up at end of scope
Benefits:
- Automatic cleanup at scope exit
- Exception-safe (cleanup happens even if errors occur)
- Follows RAII (Resource Acquisition Is Initialization) principles
- Prevents resource leaks in complex control flows
- Composable with multiple
declarationsusing
Async Resource Management
For async cleanup, use
await using with Symbol.asyncDispose:
class DatabaseConnection { #conn: Connection; constructor(conn: Connection) { this.#conn = conn; } async [Symbol.asyncDispose]() { await this.#conn.close(); console.log("Database connection closed"); } } // Automatically closes when scope exits await using db = new DatabaseConnection(conn); await db.query("SELECT * FROM users"); // Connection closed here, even if query throws
When to Use using
usingUse
using for any resource that needs cleanup:
- Timers (setInterval, setTimeout)
- File handles
- Database connections
- Network connections
- Locks and mutexes
- Any object with teardown logic
Async Iteration Over Polling
The Problem with Traditional Polling
Old Pattern:
// Traditional polling - not cancelable, drift-prone let running = true; function poll() { if (!running) return; // ...check something... setTimeout(poll, 1000); } poll(); running = false; // Unreliable cancellation
Problems:
- Timer drift accumulates over time
- Race conditions with cancellation flag
- Hard to compose or integrate with other async code
- Not part of the structured concurrency model
Async Generators for Polling
New Pattern:
// Async generator - naturally cancelable async function* interval(ms: number) { while (true) { yield; await new Promise((r) => setTimeout(r, ms)); } } // Usage with for-await-of for await (const _ of interval(1000)) { // ...do work... if (shouldStop()) break; // Clean, immediate cancellation }
Benefits:
- Natural cancellation by breaking the loop
- No timer drift - explicit timing control
- Composable with Promise.race, yield*, etc.
- Integrates with all async iterable APIs
- Clear control flow
Advanced Polling Patterns
Polling with timeout:
async function* intervalWithTimeout(intervalMs: number, timeoutMs: number) { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { yield; await new Promise((r) => setTimeout(r, intervalMs)); } } for await (const _ of intervalWithTimeout(1000, 10000)) { // Polls every 1s, stops after 10s await checkCondition(); }
Async/Promise Best Practices
Remove Unnecessary async
asyncIncorrect:
// BAD - Unnecessary async wrapper async function validateMemory(content: string): boolean { if (content.trim().length === 0) { throw new Error("Content cannot be empty"); } return true; }
Correct:
// GOOD - No async needed function validateMemory(content: string): boolean { if (content.trim().length === 0) { throw new Error("Content cannot be empty"); } return true; }
Interface Compliance with Promise.resolve()
When implementing an interface that requires
Promise<T> but your logic is synchronous:
interface QueueMessageHandler { handle(message: QueueMessage): Promise<void>; } // GOOD - Return Promise.resolve() explicitly class SyncMessageHandler implements QueueMessageHandler { handle(message: QueueMessage): Promise<void> { this.processSync(message); return Promise.resolve(); } } // GOOD - Return Promise.reject() for errors class ValidatingHandler implements QueueMessageHandler { handle(message: QueueMessage): Promise<void> { if (message.corrupted) { return Promise.reject(new Error("Message corrupted")); } return Promise.resolve(); } }
Only Use async
When You Actually await
asyncawait// GOOD - async because we await async function processMemory(content: string): Promise<ProcessedMemory> { const embedding = await ollama.generateEmbedding(content); const entities = await ollama.extractEntities(content); return { content, embedding, entities }; } // GOOD - no async because no await function validateConfig(config: Config): boolean { return config.apiKey !== undefined; }
Error Handling
Deno's Class-Based Errors
Old Pattern (Node.js):
// String-based error codes try { fs.readFileSync('file'); } catch (err) { if (err.code === 'ENOENT') { // handle not found } }
New Pattern (Deno):
// Type-safe error classes try { await Deno.readTextFile("file.txt"); } catch (err) { if (err instanceof Deno.errors.NotFound) { // handle not found } else if (err instanceof Deno.errors.PermissionDenied) { // handle permission error } }
Available Deno Error Classes
Deno.errors.NotFound Deno.errors.PermissionDenied Deno.errors.ConnectionRefused Deno.errors.ConnectionReset Deno.errors.ConnectionAborted Deno.errors.NotConnected Deno.errors.AddrInUse Deno.errors.AddrNotAvailable Deno.errors.BrokenPipe Deno.errors.AlreadyExists Deno.errors.InvalidData Deno.errors.TimedOut Deno.errors.Interrupted Deno.errors.WriteZero Deno.errors.UnexpectedEof Deno.errors.BadResource Deno.errors.Busy
Error Handling Best Practices
// Specific error handling async function loadConfig(path: string): Promise<Config> { try { const content = await Deno.readTextFile(path); return JSON.parse(content); } catch (err) { if (err instanceof Deno.errors.NotFound) { throw new Error(`Config file not found: ${path}`); } else if (err instanceof Deno.errors.PermissionDenied) { throw new Error(`Permission denied reading config: ${path}`); } else if (err instanceof SyntaxError) { throw new Error(`Invalid JSON in config: ${path}`); } throw err; // Re-throw unknown errors } }
Benefits:
- Type-safe - no magic string codes
- Better IDE autocomplete
- Easier refactoring
- Clear error hierarchies
Web-Standard APIs
HTTP Server
Old (Node.js):
// Node.js style const http = require("http"); http.createServer((req, res) => { res.writeHead(200); res.end("OK"); }).listen(8000);
New (Deno):
// Deno - serverless-compatible Deno.serve((req) => new Response("OK"));
Benefits:
- Simpler, cleaner API
- Native Request/Response objects
- Works with serverless platforms
- No legacy API constraints
File Operations
Old (Node.js):
// Node.js callbacks fs.readFile('file.txt', 'utf8', (err, data) => { if (err) throw err; console.log(data); });
New (Deno):
// Deno - async/await const data = await Deno.readTextFile("file.txt"); console.log(data);
Fetch API
Deno has native
fetch() with no imports needed:
// Native fetch - no imports const response = await fetch("https://api.example.com/data"); const data = await response.json();
Streams
Use Web Streams API:
// Web Streams const file = await Deno.open("large-file.txt"); const readable = file.readable; for await (const chunk of readable) { // Process chunk }
Crypto
Use Web Crypto API:
// Web Crypto const data = new TextEncoder().encode("hello"); const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
Standard Library Utilities
Path Operations
import { join, dirname, basename } from "@std/path"; const fullPath = join("/users", "alice", "documents", "file.txt"); const dir = dirname(fullPath); // /users/alice/documents const file = basename(fullPath); // file.txt
File System
import { ensureDir, exists } from "@std/fs"; // Ensure directory exists await ensureDir("./data/cache"); // Check if file exists if (await exists("config.json")) { // ... }
Time-Based Identifiers
Use
@std/ulid for sortable IDs:
import { ulid } from "@std/ulid"; // Generate ULID (sortable by creation time) const id = ulid(); // 01ARZ3NDEKTSV4RRFFQ69G5FAV // ULIDs are lexicographically sortable by time const ids = [ulid(), ulid(), ulid()]; ids.sort(); // Automatically sorted by creation time
When to use ULID:
- Need UUID-like identifiers sortable by time
- Want to avoid UUID v4 random collisions
- Need efficient database indexing by creation time
- Want to extract timestamp from ID
Pattern Migration Guide
Quick Reference
| Use Case | Old Pattern | Modern (Deno) Pattern |
|---|---|---|
| Timer | + | + class w/ |
| Polling | Repeated | Async generator () |
| Cleanup | Manual try/finally | / |
| Error Handling | | |
| HTTP Server | | |
| File Reading | | |
| Environment Vars | | |
| Module Format | CommonJS () | ESM () |
Migration Examples
Timer Management:
// Old: const id = setInterval(doWork, 1000); // ... later ... clearInterval(id); // New: class Timer { #id; constructor(cb, ms) { this.#id = setInterval(cb, ms); } [Symbol.dispose]() { clearInterval(this.#id); } } using t = new Timer(doWork, 1000); // Automatically disposed at end of scope
Async Polling:
// Old: let running = true; const poll = () => { if (!running) return; doWork(); setTimeout(poll, 1000); }; poll(); running = false; // To stop // New: async function* poller(ms) { while (true) { yield; await new Promise(r => setTimeout(r, ms)); } } for await (const _ of poller(1000)) { doWork(); if (shouldStop()) break; // Natural cancellation }
File Operations:
// Old (Node.js): const fs = require('fs'); const data = fs.readFileSync('file.txt', 'utf8'); // New (Deno): const data = await Deno.readTextFile("file.txt");
Environment Variables:
// Old (Node.js): const apiKey = process.env.API_KEY; // New (Deno): const apiKey = Deno.env.get("API_KEY"); // Requires: --allow-env=API_KEY
Structured Concurrency
AbortController for Cancellation
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { signal: controller.signal }); return response; } finally { clearTimeout(timeoutId); } } // Usage: try { const response = await fetchWithTimeout("https://slow-api.com", 5000); } catch (err) { if (err.name === 'AbortError') { console.log("Request timed out"); } }
Racing Multiple Promises
// First successful response wins const response = await Promise.race([ fetch("https://api1.com/data"), fetch("https://api2.com/data"), fetch("https://api3.com/data"), ]); // All must succeed const [user, posts, comments] = await Promise.all([ fetchUser(id), fetchPosts(id), fetchComments(id), ]);
Performance Considerations
Resource Management
No Runtime Overhead:
has no performance penalty vs manual cleanupusing- More robust in exception paths
- Prevents resource leaks that degrade performance
Async Generators
Minimal Overhead:
- Async generators are efficient
- No additional allocations per iteration
- Better than callback-based patterns
Type-Only Imports
Build-Time Optimization:
// GOOD - Erased at runtime import type { User } from "./types.ts"; // BAD - Bundled even if only used for types import { User } from "./types.ts";
Summary: Modern Pattern Principles
- Use
for any resource needing cleanupusing - Prefer async generators over polling loops
- Remove
if noasync
is presentawait - Use Promise.resolve() for interface compliance
- Handle errors with Deno's class-based system
- Prefer Web Standard APIs over Node.js patterns
- Use AbortController for cancellable operations
- Leverage @std library for common operations
- Use ULID for time-based sortable IDs
- Always prefer structured, composable patterns
Additional Resources
- TC39 Explicit Resource Management: https://github.com/tc39/proposal-explicit-resource-management
- Deno Web APIs: https://docs.deno.com/runtime/manual/runtime/web_platform_apis
- Async Iterators: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator