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.
This 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 present