You see ECONNREFUSED 127.0.0.1:5432. You know it means PostgreSQL isnβt running. But your junior developer doesnβt. And neither does the you from 3 years ago who spent 45 minutes googling it.
In this tutorial, weβll build wtf β a CLI tool that explains any error message in plain English and suggests a fix. Pipe errors directly from your terminal.
What weβre building
$ wtf "ECONNREFUSED 127.0.0.1:5432"
π Error: Connection Refused (ECONNREFUSED)
Your app tried to connect to PostgreSQL on localhost:5432,
but nothing is listening on that port.
Likely causes:
1. PostgreSQL isn't running
2. It's running on a different port
3. A firewall is blocking the connection
Fix:
$ sudo systemctl start postgresql # Linux
$ brew services start postgresql # macOS
$ docker start my-postgres # Docker
Verify: $ pg_isready -h localhost -p 5432
Or pipe errors directly:
$ npm run build 2>&1 | wtf
$ python app.py 2>&1 | wtf
$ docker compose up 2>&1 | tail -5 | wtf
Prerequisites
- Node.js 20+
- An Anthropic or OpenAI API key
Step 1: Set up the project
mkdir wtf-cli && cd wtf-cli
npm init -y
npm install @anthropic-ai/sdk
Add to package.json:
{
"bin": { "wtf": "./index.js" },
"type": "module"
}
Step 2: Build the CLI
#!/usr/bin/env node
// index.js
import Anthropic from '@anthropic-ai/sdk';
import { readFileSync, existsSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
// Load API key from ~/.wtf-config or environment
const configPath = join(homedir(), '.wtf-config');
const apiKey = process.env.ANTHROPIC_API_KEY ||
(existsSync(configPath) ? readFileSync(configPath, 'utf-8').trim() : null);
if (!apiKey) {
console.error('Set ANTHROPIC_API_KEY or create ~/.wtf-config with your key');
process.exit(1);
}
const anthropic = new Anthropic({ apiKey });
// Get error from argument or stdin
let errorText = process.argv.slice(2).join(' ');
if (!errorText) {
// Read from stdin (piped input)
errorText = readFileSync('/dev/stdin', 'utf-8').trim();
}
if (!errorText) {
console.log('Usage: wtf "error message" or command 2>&1 | wtf');
process.exit(0);
}
// Truncate very long errors
if (errorText.length > 3000) {
errorText = errorText.slice(-3000); // Keep the last 3000 chars (most relevant)
}
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 512,
messages: [{
role: 'user',
content: `You are a senior developer helping debug an error. Explain this error in plain English:
${errorText}
Format your response as:
π Error: [brief name]
[1-2 sentence explanation of what went wrong]
Likely causes:
[numbered list, most common first]
Fix:
[specific commands or code to fix it, with comments]
Keep it concise. No preamble. Assume the developer is intermediate level.`
}],
});
console.log(response.content[0].text);
Step 3: Make it executable
chmod +x index.js
npm link # Makes 'wtf' available globally
Step 4: Configure your API key
echo "sk-ant-your-key-here" > ~/.wtf-config
chmod 600 ~/.wtf-config # Restrict permissions
Step 5: Test it
# Direct error
wtf "TypeError: Cannot read properties of undefined (reading 'map')"
# Piped from a failing command
npm run build 2>&1 | wtf
# Last 10 lines of Docker logs
docker logs myapp --tail 10 2>&1 | wtf
Making it better
Add language detection
Detect the programming language from the error to give more specific advice:
function detectLanguage(error) {
if (error.includes('Traceback') || error.includes('IndentationError')) return 'Python';
if (error.includes('NullPointerException') || error.includes('java.')) return 'Java';
if (error.includes('TypeError') || error.includes('node_modules')) return 'JavaScript/Node.js';
if (error.includes('panic:') || error.includes('goroutine')) return 'Go';
if (error.includes('NullReferenceException')) return 'C#';
return 'Unknown';
}
// Add to prompt: "The error is from a ${detectLanguage(errorText)} application."
Cache common errors
Save explanations locally to avoid API calls for errors youβve seen before:
import { createHash } from 'crypto';
const cacheDir = join(homedir(), '.wtf-cache');
const hash = createHash('md5').update(errorText).digest('hex');
const cachePath = join(cacheDir, hash);
if (existsSync(cachePath)) {
console.log(readFileSync(cachePath, 'utf-8'));
process.exit(0);
}
// After getting response:
mkdirSync(cacheDir, { recursive: true });
writeFileSync(cachePath, response.content[0].text);
Use a cheaper model for simple errors
Route simple errors (connection refused, file not found) to a cheaper model and complex errors to Sonnet:
const isSimple = /ECONNREFUSED|ENOENT|EACCES|EADDRINUSE/.test(errorText);
const model = isSimple ? 'claude-haiku-3.5' : 'claude-sonnet-4-6';
Deploy as an npm package
# Update package.json
{
"name": "wtf-error",
"version": "1.0.0",
"description": "AI-powered error explainer",
"bin": { "wtf": "./index.js" }
}
npm publish
# Now anyone can: npx wtf-error "their error"
Total build time: ~30 minutes. API cost: ~$0.002 per error explanation.
Previous: Build a Slack Bot That Summarizes Channels Next: Build a Local AI Chatbot for Your Docs
Related: Build Code Snippet Manager