コールバック
コールバックは非同期処理の最も原始的なパターン。Node.jsのerror-firstコールバック、コールバック地獄の問題、そしてPromiseへの進化を理解する。
82 分で読めます40,698 文字
コールバック
コールバックは非同期処理の最も原始的なパターン。Node.jsのerror-firstコールバック、コールバック地獄の問題、そしてPromiseへの進化を理解する。
この章で学ぶこと
- コールバックの仕組みと使い方を理解する
- コールバック地獄の問題と原因を把握する
- error-first パターンの意味を学ぶ
- 各言語におけるコールバックの実装を比較する
- コールバックから Promise への移行パターンを習得する
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
1. コールバックの基本
1.1 コールバックとは
コールバック = 「処理が完了したら呼んでね」と渡す関数
同期:
const result = readFile("data.txt");
console.log(result);
非同期(コールバック):
readFile("data.txt", (error, result) => {
console.log(result);
});
// readFile は即座に戻る。結果は後でコールバックに届く
コールバックの分類:
1. 同期コールバック: map, filter, sort など(即座に実行される)
2. 非同期コールバック: I/O完了後に呼ばれる(setTimeout, fs.readFileなど)
1.2 同期コールバック vs 非同期コールバック
// === 同期コールバック ===
// 関数の引数として渡され、その場で即座に実行される
// Array.map: 各要素に対してコールバックを適用
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((n) => n * 2); // [2, 4, 6, 8, 10]
// Array.filter: コールバックがtrueを返す要素だけ残す
const evens = numbers.filter((n) => n % 2 === 0); // [2, 4]
// Array.reduce: 累積値を計算
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15
// Array.sort: コールバックで比較ロジックを注入
const users = [
{ name: "田中", age: 30 },
{ name: "山田", age: 25 },
{ name: "鈴木", age: 35 },
];
users.sort((a, b) => a.age - b.age);
// [{ name: "山田", age: 25 }, { name: "田中", age: 30 }, { name: "鈴木", age: 35 }]
// Array.forEach: 各要素に副作用を実行
numbers.forEach((n) => {
console.log(n);
});
// Array.find: 条件に合う最初の要素を返す
const firstEven = numbers.find((n) => n % 2 === 0); // 2
// これらは全て「同期コールバック」
// → 関数が返る時点で全ての処理が完了している// === 非同期コールバック ===
// 関数に渡され、I/O完了後や一定時間後に実行される
const fs = require('fs');
// Node.js ファイル読み込み
console.log('1. 読み込み開始');
fs.readFile('/path/to/file', 'utf8', (err, data) => {
// この関数は「後で」呼ばれる(ファイル読み込み完了時)
console.log('3. ファイル読み込み完了:', data);
});
console.log('2. 読み込み命令発行後(まだ完了していない)');
// 出力順序:
// 1. 読み込み開始
// 2. 読み込み命令発行後(まだ完了していない)
// 3. ファイル読み込み完了: (ファイルの内容)1.3 イベントリスナーとしてのコールバック
// ブラウザ: イベントリスナー
document.getElementById('btn').addEventListener('click', (event) => {
console.log('Clicked!', event.target);
});
// 複数のイベントを登録
const button = document.getElementById('submit');
button.addEventListener('click', handleClick);
button.addEventListener('mouseenter', handleHover);
button.addEventListener('mouseleave', handleLeave);
function handleClick(event) {
event.preventDefault();
console.log('Button clicked');
}
function handleHover(event) {
event.target.style.backgroundColor = '#f0f0f0';
}
function handleLeave(event) {
event.target.style.backgroundColor = '';
}
// イベントリスナーの解除
button.removeEventListener('click', handleClick);1.4 タイマーコールバック
// setTimeout: 指定時間後に1回実行
setTimeout(() => {
console.log('3秒後に実行');
}, 3000);
// setInterval: 指定間隔で繰り返し実行
const intervalId = setInterval(() => {
console.log('1秒ごとに実行');
}, 1000);
// 停止
setTimeout(() => {
clearInterval(intervalId);
console.log('タイマー停止');
}, 5000);
// requestAnimationFrame: 描画フレームごとに実行(ブラウザ)
function animate(timestamp) {
// アニメーション処理
updatePosition(timestamp);
render();
// 次のフレームを要求
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);2. Node.js の error-first コールバック
2.1 基本パターン
Node.js の規約(error-first callback):
callback(error, result)
→ 第1引数: エラー(成功時は null)
→ 第2引数: 結果(エラー時は undefined)
利点:
- エラーチェックが統一的
- エラーを無視しにくい(第1引数を見る習慣)
問題:
- 毎回 if (err) のチェックが必要
- 型安全性がない(any)
- ネストが深くなりやすい
const fs = require('fs');
// error-first コールバックの基本
fs.readFile('/path/to/file', 'utf8', (err, data) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Data:', data);
});
// 書き込み
fs.writeFile('/path/to/output', 'Hello, World!', 'utf8', (err) => {
if (err) {
console.error('Write failed:', err.message);
return;
}
console.log('File written successfully');
});
// ディレクトリ読み取り
fs.readdir('/path/to/dir', (err, files) => {
if (err) {
console.error('Failed to read directory:', err.message);
return;
}
console.log('Files:', files);
});2.2 error-first パターンの実装
// error-first の自作関数
function readJsonFile(path, callback) {
fs.readFile(path, 'utf8', (err, data) => {
if (err) {
callback(err, null);
return;
}
try {
const parsed = JSON.parse(data);
callback(null, parsed);
} catch (parseError) {
callback(parseError, null);
}
});
}
// 使用
readJsonFile('config.json', (err, config) => {
if (err) {
console.error('Failed to read config:', err.message);
return;
}
console.log('Config loaded:', config);
});// 非同期データベース操作(コールバックスタイル)
function getUser(userId, callback) {
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, rows) => {
if (err) {
callback(err, null);
return;
}
if (rows.length === 0) {
callback(new Error(`User ${userId} not found`), null);
return;
}
callback(null, rows[0]);
});
}
function getUserOrders(userId, callback) {
db.query('SELECT * FROM orders WHERE user_id = ?', [userId], (err, rows) => {
if (err) {
callback(err, null);
return;
}
callback(null, rows);
});
}
// HTTPリクエスト(コールバックスタイル)
const http = require('http');
function fetchJSON(url, callback) {
http.get(url, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('error', (err) => {
callback(err, null);
});
res.on('end', () => {
try {
const parsed = JSON.parse(data);
callback(null, parsed);
} catch (parseErr) {
callback(parseErr, null);
}
});
}).on('error', (err) => {
callback(err, null);
});
}
// 使用
fetchJSON('http://api.example.com/users/1', (err, user) => {
if (err) {
console.error('Failed to fetch user:', err.message);
return;
}
console.log('User:', user);
});2.3 コールバックの設計パターン
// パターン1: 設定オブジェクトとコールバック
function connectToDatabase(options, callback) {
const { host, port, database, user, password } = options;
const connection = new DatabaseConnection({
host, port, database, user, password
});
connection.connect((err) => {
if (err) {
callback(err, null);
return;
}
// 接続成功: マイグレーションチェック
connection.checkMigrations((err, needsMigration) => {
if (err) {
connection.close();
callback(err, null);
return;
}
if (needsMigration) {
connection.runMigrations((err) => {
if (err) {
connection.close();
callback(err, null);
return;
}
callback(null, connection);
});
} else {
callback(null, connection);
}
});
});
}
// パターン2: EventEmitter スタイル
const EventEmitter = require('events');
class FileProcessor extends EventEmitter {
process(filePath) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
this.emit('error', err);
return;
}
this.emit('data', data);
const lines = data.split('\n');
this.emit('line-count', lines.length);
for (const line of lines) {
this.emit('line', line);
}
this.emit('complete', { totalLines: lines.length });
});
}
}
// 使用
const processor = new FileProcessor();
processor.on('data', (data) => {
console.log(`Loaded ${data.length} bytes`);
});
processor.on('line', (line) => {
// 各行を処理
});
processor.on('complete', ({ totalLines }) => {
console.log(`Processed ${totalLines} lines`);
});
processor.on('error', (err) => {
console.error('Error:', err.message);
});
processor.process('large-file.txt');3. コールバック地獄(Callback Hell)
3.1 問題の本質
// ❌ コールバック地獄: ネストが深くなり可読性が崩壊
getUser(userId, (err, user) => {
if (err) { handleError(err); return; }
getOrders(user.id, (err, orders) => {
if (err) { handleError(err); return; }
getOrderDetails(orders[0].id, (err, details) => {
if (err) { handleError(err); return; }
getShippingInfo(details.shippingId, (err, shipping) => {
if (err) { handleError(err); return; }
getTrackingInfo(shipping.trackingId, (err, tracking) => {
if (err) { handleError(err); return; }
// ここまで5段階のネスト
console.log(tracking);
});
});
});
});
});
// 問題:
// 1. 横に広がる「ピラミッド型」コード
// 2. エラーハンドリングの重複
// 3. 変数スコープの管理困難
// 4. 処理の流れが追いにくい
// 5. テストが書きにくい
// 6. 制御フロー(条件分岐、ループ)の実装が複雑3.2 実務で遭遇する典型的なコールバック地獄
// ECサイトの注文処理(コールバック地獄版)
function processOrder(userId, cartId, paymentInfo, callback) {
// 1. ユーザー認証
authenticateUser(userId, (err, user) => {
if (err) { callback(err); return; }
// 2. カート取得
getCart(cartId, (err, cart) => {
if (err) { callback(err); return; }
// 3. 在庫チェック
checkInventory(cart.items, (err, availability) => {
if (err) { callback(err); return; }
if (!availability.allAvailable) {
callback(new Error('Some items are out of stock'));
return;
}
// 4. 金額計算
calculateTotal(cart, user, (err, total) => {
if (err) { callback(err); return; }
// 5. 支払い処理
processPayment(paymentInfo, total, (err, paymentResult) => {
if (err) {
// 支払い失敗時の在庫ロールバック
releaseInventory(cart.items, (rollbackErr) => {
if (rollbackErr) {
console.error('Rollback failed:', rollbackErr);
}
callback(err);
});
return;
}
// 6. 注文作成
createOrder(user, cart, paymentResult, (err, order) => {
if (err) {
// 注文作成失敗時の支払い取り消し
refundPayment(paymentResult.id, (refundErr) => {
if (refundErr) {
console.error('Refund failed:', refundErr);
}
callback(err);
});
return;
}
// 7. 通知送信
sendOrderConfirmation(user.email, order, (err) => {
if (err) {
console.error('Email failed:', err);
// メール失敗は無視して成功扱い
}
callback(null, order);
});
});
});
});
});
});
});
}3.3 改善テクニック1: 名前付き関数で分離
// やや改善: 名前付き関数で分離
function handleTracking(err, tracking) {
if (err) { handleError(err); return; }
console.log(tracking);
}
function handleShipping(err, shipping) {
if (err) { handleError(err); return; }
getTrackingInfo(shipping.trackingId, handleTracking);
}
function handleDetails(err, details) {
if (err) { handleError(err); return; }
getShippingInfo(details.shippingId, handleShipping);
}
function handleOrders(err, orders) {
if (err) { handleError(err); return; }
getOrderDetails(orders[0].id, handleDetails);
}
function handleUser(err, user) {
if (err) { handleError(err); return; }
getOrders(user.id, handleOrders);
}
// エントリポイント
getUser(userId, handleUser);
// 改善点: ネストが浅い
// 残る問題: 関数が逆順で定義され、流れが追いにくい3.4 改善テクニック2: async ライブラリ
// async.js ライブラリを使ったフロー制御
const async = require('async');
// async.waterfall: 直列実行(前の結果を次に渡す)
async.waterfall([
// Step 1: ユーザー取得
(cb) => getUser(userId, cb),
// Step 2: 注文取得(userは前のステップの結果)
(user, cb) => getOrders(user.id, (err, orders) => {
cb(err, user, orders);
}),
// Step 3: 注文詳細取得
(user, orders, cb) => getOrderDetails(orders[0].id, (err, details) => {
cb(err, user, orders, details);
}),
// Step 4: 配送情報取得
(user, orders, details, cb) => {
getShippingInfo(details.shippingId, cb);
},
], (err, shippingInfo) => {
if (err) {
handleError(err);
return;
}
console.log('Shipping:', shippingInfo);
});
// async.parallel: 並行実行
async.parallel({
users: (cb) => fetchUsers(cb),
orders: (cb) => fetchOrders(cb),
products: (cb) => fetchProducts(cb),
}, (err, results) => {
if (err) {
handleError(err);
return;
}
console.log(results.users, results.orders, results.products);
});
// async.series: 直列実行(結果は別々に)
async.series([
(cb) => createBackup(cb),
(cb) => runMigrations(cb),
(cb) => verifyData(cb),
], (err, results) => {
if (err) {
console.error('Pipeline failed:', err);
return;
}
console.log('All steps completed');
});
// async.eachLimit: 並行数制限付き反復
const urls = ['url1', 'url2', 'url3', /* ... */];
async.eachLimit(urls, 5, (url, cb) => {
fetchAndProcess(url, cb);
}, (err) => {
if (err) {
console.error('Processing failed:', err);
return;
}
console.log('All URLs processed');
});3.5 改善テクニック3: 制御フロー抽象化
// 独自のフロー制御関数
function waterfall(tasks, finalCallback) {
let index = 0;
function next(err, ...args) {
if (err) {
finalCallback(err);
return;
}
if (index >= tasks.length) {
finalCallback(null, ...args);
return;
}
const task = tasks[index++];
try {
task(...args, next);
} catch (e) {
finalCallback(e);
}
}
next(null);
}
// 使用
waterfall([
(cb) => getUser(userId, cb),
(user, cb) => getOrders(user.id, cb),
(orders, cb) => getOrderDetails(orders[0].id, cb),
], (err, details) => {
if (err) {
console.error(err);
return;
}
console.log('Details:', details);
});
// 並行実行関数
function parallel(tasks, finalCallback) {
const results = {};
let completed = 0;
let hasError = false;
const keys = Object.keys(tasks);
keys.forEach((key) => {
taskskey => {
if (hasError) return;
if (err) {
hasError = true;
finalCallback(err);
return;
}
results[key] = result;
completed++;
if (completed === keys.length) {
finalCallback(null, results);
}
});
});
}4. 各言語のコールバックパターン
4.1 Python のコールバック
import threading
import time
from typing import Callable, Optional, Any
# Python でのコールバックパターン
# 基本的なコールバック
def fetch_data(url: str, on_success: Callable, on_error: Callable) -> None:
"""非同期的にデータを取得(スレッドベース)"""
def worker():
try:
import urllib.request
response = urllib.request.urlopen(url)
data = response.read().decode('utf-8')
on_success(data)
except Exception as e:
on_error(e)
thread = threading.Thread(target=worker)
thread.start()
# 使用
def handle_success(data):
print(f"Received: {data[:100]}...")
def handle_error(error):
print(f"Error: {error}")
fetch_data("https://api.example.com/data", handle_success, handle_error)
# デコレータとしてのコールバック
def retry(max_retries: int = 3, delay: float = 1.0):
"""リトライデコレータ"""
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(delay * (2 ** attempt))
return wrapper
return decorator
@retry(max_retries=3, delay=0.5)
def unreliable_api_call():
"""不安定なAPI呼び出し"""
import random
if random.random() < 0.5:
raise ConnectionError("Connection failed")
return {"status": "ok"}
# コンテキストマネージャー + コールバック
class TimedOperation:
"""操作の時間を計測し、コールバックで報告"""
def __init__(self, name: str, on_complete: Callable[[str, float], None]):
self.name = name
self.on_complete = on_complete
self.start_time = None
def __enter__(self):
self.start_time = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.time() - self.start_time
self.on_complete(self.name, elapsed)
return False
# 使用
def log_timing(name: str, elapsed: float):
print(f"[TIMING] {name}: {elapsed:.3f}s")
with TimedOperation("data_processing", log_timing):
time.sleep(0.5) # 何らかの処理
# 出力: [TIMING] data_processing: 0.501s4.2 Rust のコールバック
use std::thread;
use std::sync::mpsc;
// Rust: クロージャをコールバックとして使用
// 所有権と寿命の制約がある
// 基本的なコールバック
fn process_async<F>(data: Vec<i32>, callback: F)
where
F: FnOnce(Vec<i32>) + Send + 'static,
{
thread::spawn(move || {
let result: Vec<i32> = data.iter().map(|x| x * 2).collect();
callback(result);
});
}
// 使用
fn main() {
process_async(vec![1, 2, 3, 4, 5], |result| {
println!("Result: {:?}", result);
});
thread::sleep(std::time::Duration::from_secs(1));
}
// Result型でエラーハンドリング
fn fetch_data<F>(url: &str, callback: F)
where
F: FnOnce(Result<String, Box<dyn std::error::Error>>) + Send + 'static,
{
let url = url.to_string();
thread::spawn(move || {
let result = reqwest::blocking::get(&url)
.and_then(|resp| resp.text());
match result {
Ok(body) => callback(Ok(body)),
Err(e) => callback(Err(Box::new(e))),
}
});
}
// トレイトオブジェクトを使ったコールバック
trait EventHandler: Send {
fn on_data(&self, data: &[u8]);
fn on_error(&self, error: &str);
fn on_complete(&self);
}
struct DataProcessor {
handler: Box<dyn EventHandler>,
}
impl DataProcessor {
fn new(handler: Box<dyn EventHandler>) -> Self {
DataProcessor { handler }
}
fn process(&self, data: &[u8]) {
if data.is_empty() {
self.handler.on_error("Empty data");
return;
}
self.handler.on_data(data);
self.handler.on_complete();
}
}4.3 Go のコールバック
package main
import (
"fmt"
"io/ioutil"
"net/http"
"time"
)
// Go: 関数を第一級市民として使用
// ただし Go では goroutine + channel の方がイディオマティック
// コールバック型の定義
type ResultCallback func(data []byte, err error)
type ProgressCallback func(current, total int)
// コールバック付きHTTPリクエスト
func fetchWithCallback(url string, callback ResultCallback) {
go func() {
resp, err := http.Get(url)
if err != nil {
callback(nil, err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
callback(nil, err)
return
}
callback(body, nil)
}()
}
// プログレス付きダウンロード
func downloadWithProgress(url string, progress ProgressCallback, done ResultCallback) {
go func() {
resp, err := http.Get(url)
if err != nil {
done(nil, err)
return
}
defer resp.Body.Close()
contentLength := int(resp.ContentLength)
data := make([]byte, 0, contentLength)
buf := make([]byte, 4096)
received := 0
for {
n, err := resp.Body.Read(buf)
if n > 0 {
data = append(data, buf[:n]...)
received += n
progress(received, contentLength)
}
if err != nil {
break
}
}
done(data, nil)
}()
}
func main() {
// 使用例
fetchWithCallback("https://api.example.com/data", func(data []byte, err error) {
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Received:", len(data), "bytes")
})
// Go のイディオム: channel の方が好ましい
ch := make(chan []byte, 1)
errCh := make(chan error, 1)
go func() {
resp, err := http.Get("https://api.example.com/data")
if err != nil {
errCh <- err
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
ch <- body
}()
select {
case data := <-ch:
fmt.Println("Received:", len(data), "bytes")
case err := <-errCh:
fmt.Println("Error:", err)
case <-time.After(5 * time.Second):
fmt.Println("Timeout")
}
}4.4 C# のコールバック
using System;
using System.Net.Http;
using System.Threading.Tasks;
// C#: デリゲートとイベントによるコールバック
// デリゲート定義
public delegate void DataCallback(string data);
public delegate void ErrorCallback(Exception error);
public class AsyncFetcher
{
// イベントベースのコールバック
public event EventHandler<DataEventArgs> DataReceived;
public event EventHandler<ErrorEventArgs> ErrorOccurred;
public event EventHandler Completed;
// コールバックを受け取るメソッド
public void FetchData(string url, Action<string> onSuccess, Action<Exception> onError)
{
Task.Run(async () =>
{
try
{
using var client = new HttpClient();
var data = await client.GetStringAsync(url);
onSuccess(data);
}
catch (Exception ex)
{
onError(ex);
}
});
}
// イベントを発火するメソッド
public async void FetchDataEvent(string url)
{
try
{
using var client = new HttpClient();
var data = await client.GetStringAsync(url);
DataReceived?.Invoke(this, new DataEventArgs(data));
Completed?.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(this, new ErrorEventArgs(ex));
}
}
}
// 使用
var fetcher = new AsyncFetcher();
// ラムダ式でコールバック
fetcher.FetchData(
"https://api.example.com/data",
data => Console.WriteLine($"Success: {data.Length} chars"),
error => Console.WriteLine($"Error: {error.Message}")
);
// イベントでコールバック
fetcher.DataReceived += (sender, args) =>
{
Console.WriteLine($"Data received: {args.Data.Length} chars");
};
fetcher.ErrorOccurred += (sender, args) =>
{
Console.WriteLine($"Error: {args.Error.Message}");
};
fetcher.FetchDataEvent("https://api.example.com/data");5. 高階関数としてのコールバック
5.1 関数合成とコールバック
// コールバックは「高階関数」の一種
// 「何をするか」を引数として渡す
// 戦略パターン: コールバックでアルゴリズムを注入
function sortUsers(users, comparator) {
return [...users].sort(comparator);
}
const users = [
{ name: "田中", age: 30, score: 85 },
{ name: "山田", age: 25, score: 92 },
{ name: "鈴木", age: 35, score: 78 },
];
// 年齢順
const byAge = sortUsers(users, (a, b) => a.age - b.age);
// スコア順(降順)
const byScore = sortUsers(users, (a, b) => b.score - a.score);
// 名前順
const byName = sortUsers(users, (a, b) => a.name.localeCompare(b.name, 'ja'));// ミドルウェアパターン(Express スタイル)
type Middleware = (req: Request, res: Response, next: () => void) => void;
class Router {
private middlewares: Middleware[] = [];
use(middleware: Middleware): void {
this.middlewares.push(middleware);
}
handle(req: Request, res: Response): void {
let index = 0;
const next = () => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index++];
middleware(req, res, next);
}
};
next();
}
}
// 使用
const router = new Router();
// ログミドルウェア
router.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// 認証ミドルウェア
router.use((req, res, next) => {
if (!req.headers.authorization) {
res.status(401).send('Unauthorized');
return;
}
next();
});
// ハンドラー
router.use((req, res, next) => {
res.json({ message: 'Hello, World!' });
});5.2 コールバックのカリー化
// カリー化されたコールバック
function createLogger(prefix: string) {
return function(message: string) {
console.log(`[${prefix}] ${new Date().toISOString()} ${message}`);
};
}
const infoLog = createLogger('INFO');
const errorLog = createLogger('ERROR');
const debugLog = createLogger('DEBUG');
infoLog('Server started'); // [INFO] 2024-01-01T00:00:00.000Z Server started
errorLog('Connection lost'); // [ERROR] 2024-01-01T00:00:00.000Z Connection lost
// コールバックファクトリ
function createRetryCallback<T>(
fn: (callback: (err: Error | null, result?: T) => void) => void,
maxRetries: number,
delay: number,
): Promise<T> {
return new Promise((resolve, reject) => {
let attempts = 0;
function attempt() {
fn((err, result) => {
if (!err) {
resolve(result!);
return;
}
attempts++;
if (attempts >= maxRetries) {
reject(err);
return;
}
setTimeout(attempt, delay * Math.pow(2, attempts));
});
}
attempt();
});
}
// 使用
const result = await createRetryCallback(
(cb) => fetchData('https://api.example.com/data', cb),
3,
1000,
);6. コールバックの落とし穴
6.1 Zalgo問題(同期・非同期の混在)
// ❌ Zalgo: 条件によって同期・非同期が変わる
function getData(cache, key, callback) {
if (cache[key]) {
// ⚠️ 同期的にコールバックを呼んでいる
callback(null, cache[key]);
} else {
// 非同期的にコールバックを呼んでいる
db.query(key, (err, data) => {
if (!err) cache[key] = data;
callback(err, data);
});
}
}
// 問題: 呼び出し側のコードの実行順序が予測不能
let result;
getData(cache, 'key', (err, data) => {
result = data;
});
// result が設定されているかどうかは cache の状態に依存
// → 非常にバグを生みやすい
// ✅ 修正: 常に非同期にする
function getDataFixed(cache, key, callback) {
if (cache[key]) {
// process.nextTick で非同期化
process.nextTick(() => callback(null, cache[key]));
} else {
db.query(key, (err, data) => {
if (!err) cache[key] = data;
callback(err, data);
});
}
}
// ✅ より良い修正: queueMicrotask(ブラウザ/Node.js共通)
function getDataBetter(cache, key, callback) {
if (cache[key]) {
queueMicrotask(() => callback(null, cache[key]));
} else {
db.query(key, (err, data) => {
if (!err) cache[key] = data;
callback(err, data);
});
}
}6.2 コールバックの二重呼び出し
// ❌ コールバックが2回呼ばれる可能性
function processFile(path, callback) {
fs.readFile(path, 'utf8', (err, data) => {
if (err) {
callback(err);
// ⚠️ return を忘れている!
}
// エラー時もここが実行される
const processed = transform(data); // data は undefined → エラー
callback(null, processed);
});
}
// ✅ 修正: return で早期脱出
function processFileFixed(path, callback) {
fs.readFile(path, 'utf8', (err, data) => {
if (err) {
callback(err);
return; // ← 重要
}
try {
const processed = transform(data);
callback(null, processed);
} catch (transformErr) {
callback(transformErr);
}
});
}
// ✅ より安全: once ラッパー
function once(fn) {
let called = false;
return function(...args) {
if (called) {
console.warn('Callback called more than once');
return;
}
called = true;
fn(...args);
};
}
function processFileSafe(path, callback) {
const safeCallback = once(callback);
fs.readFile(path, 'utf8', (err, data) => {
if (err) {
safeCallback(err);
return;
}
try {
const processed = transform(data);
safeCallback(null, processed);
} catch (transformErr) {
safeCallback(transformErr);
}
});
}6.3 エラーの飲み込み
// ❌ コールバック内のエラーが外に伝播しない
try {
getUser(userId, (err, user) => {
if (err) throw err; // ← これはキャッチされない!
// コールバックは別のコールスタックで実行されるため
// try-catch は効かない
});
} catch (err) {
// ここには到達しない
console.error(err);
}
// ✅ コールバック内でエラーハンドリング
getUser(userId, (err, user) => {
if (err) {
console.error('Error:', err.message);
// エラーリカバリーやアラート
return;
}
// 正常処理
});
// ✅ ドメインでエラーをキャッチ(非推奨だが参考として)
const domain = require('domain');
const d = domain.create();
d.on('error', (err) => {
console.error('Domain caught:', err);
});
d.run(() => {
getUser(userId, (err, user) => {
if (err) throw err; // ドメインがキャッチ
});
});6.4 メモリリーク
// ❌ クロージャによるメモリリーク
function createConnection(config) {
const connection = new DatabaseConnection(config);
const largeBuffer = Buffer.alloc(100 * 1024 * 1024); // 100MB
return {
query(sql, callback) {
// largeBuffer はクロージャで保持される(使っていなくても)
connection.execute(sql, (err, rows) => {
callback(err, rows);
});
},
close() {
connection.close();
}
};
}
// ✅ 修正: 不要な参照を持たない
function createConnectionFixed(config) {
const connection = new DatabaseConnection(config);
// largeBuffer は関数スコープ外
function processLargeData() {
const largeBuffer = Buffer.alloc(100 * 1024 * 1024);
// 使用後に参照が消える
return transform(largeBuffer);
}
return {
query(sql, callback) {
connection.execute(sql, callback);
},
close() {
connection.close();
}
};
}
// ❌ イベントリスナーの累積
function setupHandler(element) {
// 呼ばれるたびにリスナーが追加される
element.addEventListener('click', () => {
doSomething();
});
}
// ✅ 修正: 既存のリスナーを解除
function setupHandlerFixed(element) {
// 名前付き関数で参照を保持
if (element._clickHandler) {
element.removeEventListener('click', element._clickHandler);
}
element._clickHandler = () => {
doSomething();
};
element.addEventListener('click', element._clickHandler);
}
// ✅ さらに良い: AbortController を使用
function setupHandlerModern(element) {
const controller = new AbortController();
element.addEventListener('click', () => {
doSomething();
}, { signal: controller.signal });
// クリーンアップ
return () => controller.abort();
}7. コールバックから Promise への移行
7.1 手動 Promise 化
// 手動でPromise化
function readFilePromise(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// 使用
readFilePromise('file.txt')
.then(data => console.log(data))
.catch(err => console.error(err));
// async/await版
async function main() {
try {
const data = await readFilePromise('file.txt');
console.log(data);
} catch (err) {
console.error(err);
}
}7.2 util.promisify
// Node.js: util.promisify でコールバックを Promise に変換
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const readdir = promisify(fs.readdir);
// コールバック版
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// Promise版
readFile('file.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));
// async/await版
async function main() {
try {
const data = await readFile('file.txt', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}
}
// fs/promises(Node.js 14+)
const fsPromises = require('fs/promises');
async function modernFileOps() {
const data = await fsPromises.readFile('file.txt', 'utf8');
await fsPromises.writeFile('output.txt', data.toUpperCase());
const files = await fsPromises.readdir('.');
console.log(files);
}7.3 汎用 promisify 関数
// 汎用的な promisify 実装
function promisify<T>(
fn: (...args: [...any[], (err: Error | null, result: T) => void]) => void
): (...args: any[]) => Promise<T> {
return function (...args: any[]): Promise<T> {
return new Promise((resolve, reject) => {
fn(...args, (err: Error | null, result: T) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
};
}
// 複数の戻り値を持つコールバックの promisify
function promisifyMultiResult(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, ...results) => {
if (err) {
reject(err);
} else {
resolve(results);
}
});
});
};
}
// EventEmitter を Promise に変換
function waitForEvent(emitter, eventName, timeout = 5000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Timeout waiting for event: ${eventName}`));
}, timeout);
emitter.once(eventName, (...args) => {
clearTimeout(timer);
resolve(args.length === 1 ? args[0] : args);
});
emitter.once('error', (err) => {
clearTimeout(timer);
reject(err);
});
});
}
// 使用
const server = createServer();
const connection = await waitForEvent(server, 'connection', 10000);7.4 コールバック API のラッパークラス
// レガシーなコールバック API をモダンにラップ
class DatabaseWrapper {
private db: LegacyDatabase;
constructor(connectionString: string) {
this.db = new LegacyDatabase(connectionString);
}
// コールバック API を Promise でラップ
query<T>(sql: string, params?: any[]): Promise<T[]> {
return new Promise((resolve, reject) => {
this.db.query(sql, params || [], (err: Error | null, rows: T[]) => {
if (err) reject(err);
else resolve(rows);
});
});
}
// トランザクション
async transaction<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.db.beginTransaction((err: Error | null, tx: any) => {
if (err) {
reject(err);
return;
}
const context = new TransactionContext(tx);
fn(context)
.then((result) => {
tx.commit((err: Error | null) => {
if (err) reject(err);
else resolve(result);
});
})
.catch((error) => {
tx.rollback((rollbackErr: Error | null) => {
if (rollbackErr) {
console.error('Rollback failed:', rollbackErr);
}
reject(error);
});
});
});
});
}
// 接続プール
getConnection(): Promise<Connection> {
return new Promise((resolve, reject) => {
this.db.getConnection((err: Error | null, conn: any) => {
if (err) reject(err);
else resolve(new Connection(conn));
});
});
}
}
// 使用(クリーンなasync/await)
const db = new DatabaseWrapper('postgres://localhost/mydb');
async function getUserOrders(userId: string) {
const [user] = await db.query<User>('SELECT * FROM users WHERE id = $1', [userId]);
if (!user) throw new Error('User not found');
const orders = await db.query<Order>(
'SELECT * FROM orders WHERE user_id = $1',
[userId]
);
return { user, orders };
}8. 実務でのベストプラクティス
8.1 コールバック設計のルール
1. 常に error-first パターンを使う
callback(err, result) の形式を守る
2. コールバックは常に非同期で呼ぶ
Zalgo問題を避けるために process.nextTick / queueMicrotask を使う
3. コールバックは1回だけ呼ぶ
once() ラッパーで二重呼び出しを防止
4. エラーは必ずコールバック経由で伝える
throw ではなく callback(err) を使う
5. ネストは3段階以下に抑える
名前付き関数に分離、または async ライブラリを使う
6. 可能であれば Promise / async-await に移行する
新規コードではコールバックを避ける
8.2 移行戦略
// 段階的な移行戦略
// Step 1: 既存のコールバック API をラップ
const readFileAsync = promisify(fs.readFile);
// Step 2: 新しい関数は async/await で書く
async function loadConfig(): Promise<Config> {
const data = await readFileAsync('config.json', 'utf8');
return JSON.parse(data);
}
// Step 3: コールバックを受け取る関数をデュアルインターフェースにする
function getData(
key: string,
callback?: (err: Error | null, data?: Data) => void,
): Promise<Data> | void {
const promise = getDataInternal(key);
if (callback) {
promise
.then(data => callback(null, data))
.catch(err => callback(err));
return;
}
return promise;
}
// コールバックスタイルで使用
getData('key', (err, data) => {
if (err) handleError(err);
else console.log(data);
});
// Promise スタイルで使用
const data = await getData('key');実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 概念 | ポイント |
|---|---|
| コールバック | 完了時に呼ばれる関数 |
| error-first | (err, result) の規約 |
| コールバック地獄 | ネスト深化 → Promise で解決 |
| 同期コールバック | map, filter, sort, reduce |
| 非同期コールバック | I/O, タイマー, イベント |
| Zalgo問題 | 同期/非同期の混在を避ける |
| 二重呼び出し | once() ラッパーで防止 |
| メモリリーク | クロージャの参照に注意 |
コールバックの進化
コールバック(1990年代〜)
↓ 問題: コールバック地獄
Promise(ES2015 / 2015年〜)
↓ 改善: チェーン可能、エラー伝播
async/await(ES2017 / 2017年〜)
↓ 改善: 同期的な記述
Reactive Streams(RxJS等)
↓ 拡張: ストリーム処理
AsyncIterator / for-await-of(ES2018)
→ 非同期イテレーション
次に読むべきガイド
参考文献
- Node.js Documentation. "Asynchronous Programming."
- Ogden, M. "Callback Hell." callbackhell.com.
- Havoc Pennington. "Don't Release Zalgo!" blog.izs.me.
- Casciaro, M. & Mammoliti, L. "Node.js Design Patterns." Packt Publishing, 2020.
- Mozilla Developer Network. "Callback function." MDN Web Docs.
- Caolan McMahon. "async.js." github.com/caolan/async.
- Node.js API. "util.promisify." nodejs.org.
- Rust Documentation. "Closures." doc.rust-lang.org.