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.
Provides modern TypeScript patterns for Deno, including resource management with `using`, async generators for polling, and Web Standard APIs. Use when migrating from Node.js, implementing cleanup logic, or learning contemporary JS/TS patterns.
/plugin marketplace add jahanson/cc-plugins/plugin install deno-lsp@local-pluginsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Use this skill when:
Note: Always read deno-core.md first for essential configuration and practices.
usingOld Pattern:
// Manual timer cleanup - error-prone
const id = setInterval(() => {
console.log("Tick!");
}, 1000);
// Later, cleanup (easy to forget!)
clearInterval(id);
Problems:
using Statement (Deno >= v2.4)New 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:
using declarationsFor 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
usingUse using for any resource that needs cleanup:
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:
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:
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();
}
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;
}
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();
}
}
async When You Actually await// 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;
}
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
}
}
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
// 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:
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:
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);
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();
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
}
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('');
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
import { ensureDir, exists } from "@std/fs";
// Ensure directory exists
await ensureDir("./data/cache");
// Check if file exists
if (await exists("config.json")) {
// ...
}
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:
| Use Case | Old Pattern | Modern (Deno) Pattern |
|---|---|---|
| Timer | setInterval + clearInterval | using + class w/ Symbol.dispose |
| Polling | Repeated setTimeout | Async generator (for await...of) |
| Cleanup | Manual try/finally | using/await using |
| Error Handling | if (err.code === ...) | if (err instanceof Deno.errors.*) |
| HTTP Server | http.createServer | Deno.serve |
| File Reading | fs.readFileSync | await Deno.readTextFile |
| Environment Vars | process.env.VAR | Deno.env.get("VAR") |
| Module Format | CommonJS (require) | ESM (import) |
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
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");
}
}
// 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),
]);
No Runtime Overhead:
using has no performance penalty vs manual cleanupMinimal Overhead:
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";
using for any resource needing cleanupasync if no await is presentThis skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.