AozoraEpub3Converter.jsから実行したgetImageFilePathでthis.bookInfoがnullとなる。なので、getImageFilePathの引数にthis.bookInfoを追加した。
if (this.imageIndex - 1 === this.bookInfo.coverImageIndex) {
^
TypeError: Cannot read properties of null (reading 'coverImageIndex') at Epub3Writer.getImageFilePath
this.IMAGES_PATHをEpub3Writer.IMAGES_PATHに置き換えた。IMAGES_PATHというのはEPUBに画像を格納するフォルダー名。
return ../${Epub3Writer.IMAGES_PATH}${outImageFileName};
で画像ファイルのHTMLのsrcをリターンする。object Promiseなので、awaitが必要。
async await汚染
You:
asyncを使わずthenで書くことは可能か
import fs from "fs";
import sharp from "sharp";
export default class ImageInfo {
/** 画像の情報を生成
* @param {string} ext png jpg gif webp など
*/
constructor(ext, width, height) {
this.id = null; // ファイルID
this.outFileName = null; // 出力ファイル名
this.ext = ext ? ext.toLowerCase() : null;
this.width = width ?? -1;
this.height = height ?? -1;
this.outWidth = -1;
this.outHeight = -1;
this.isCover = false;
this.rotateAngle = 0;
}
// =========================
// static 生成メソッド
// =========================
/** ファイルから画像情報を生成 */
static async getImageInfoFromFile(imageFile) {
const buffer = await fs.promises.readFile(imageFile);
return await ImageInfo.getImageInfo(buffer);
}
/** 画像ストリーム / Buffer から画像情報を生成 */
static async getImageInfo(input) {
if (!input) return null;
try {
const metadata = await sharp(input).metadata();
if (!metadata || !metadata.format) return null;
return new ImageInfo(
metadata.format, // png jpeg webp gif avif など
metadata.width,
metadata.height
);
} catch (e) {
return null;
}
}
/** 既に幅高さが分かっている場合 */
static getImageInfoFromRaw(ext, width, height) {
return new ImageInfo(ext, width, height);
}
// =========================
// getter / setter
// =========================
getId() {
return this.id;
}
setId(id) {
this.id = id;
}
getOutFileName() {
return this.outFileName;
}
setOutFileName(file) {
this.outFileName = file;
}
setExt(ext) {
this.ext = ext?.toLowerCase();
}
getExt() {
return this.ext;
}
/** mime形式(image/png)を返す */
getFormat() {
if (!this.ext) return null;
return "image/" + (this.ext === "jpg" ? "jpeg" : this.ext);
}
getIsCover() {
return this.isCover;
}
setIsCover(isCover) {
this.isCover = isCover;
}
getWidth() {
return this.width;
}
setWidth(width) {
this.width = width;
}
getHeight() {
return this.height;
}
setHeight(height) {
this.height = height;
}
getOutWidth() {
return this.outWidth;
}
setOutWidth(outWidth) {
this.outWidth = outWidth;
}
getOutHeight() {
return this.outHeight;
}
setOutHeight(outHeight) {
this.outHeight = outHeight;
}
}
ChatGPT:
結論から言うと 可能です(完全に then チェーンに書き換えできます)。
async/await は単なるシンタックスシュガーなので、すべて Promise.then() に展開できます。
あなたのコードを then版に書き換えるとこうなります👇
🔁 then版(書き換え)
JavaScript
import fs from "fs";
import sharp from "sharp";
export default class ImageInfo {
constructor(ext, width, height) {
this.id \= null;
this.outFileName \= null;
this.ext \= ext ? ext.toLowerCase() : null;
this.width \= width ?? \-1;
this.height \= height ?? \-1;
this.outWidth \= \-1;
this.outHeight \= \-1;
this.isCover \= false;
this.rotateAngle \= 0;
}
// =========================
// static 生成メソッド
// =========================
/\*\* ファイルから画像情報を生成 \*/
static getImageInfoFromFile(imageFile) {
return fs.promises.readFile(imageFile)
.then(buffer => {
return ImageInfo.getImageInfo(buffer);
});
}
/\*\* 画像ストリーム / Buffer から画像情報を生成 \*/
static getImageInfo(input) {
if (!input) return Promise.resolve(null);
return sharp(input).metadata()
.then(metadata => {
if (!metadata || !metadata.format) return null;
return new ImageInfo(
metadata.format,
metadata.width,
metadata.height
);
})
.catch(() => {
return null;
});
}
/\*\* 既に幅高さが分かっている場合 \*/
static getImageInfoFromRaw(ext, width, height) {
return new ImageInfo(ext, width, height);
}
// =========================
// getter / setter
// =========================
getId() { return this.id; }
setId(id) { this.id \= id; }
getOutFileName() { return this.outFileName; }
setOutFileName(file) { this.outFileName \= file; }
setExt(ext) { this.ext \= ext?.toLowerCase(); }
getExt() { return this.ext; }
getFormat() {
if (!this.ext) return null;
return "image/" + (this.ext \=== "jpg" ? "jpeg" : this.ext);
}
getIsCover() { return this.isCover; }
setIsCover(isCover) { this.isCover \= isCover; }
getWidth() { return this.width; }
setWidth(width) { this.width \= width; }
getHeight() { return this.height; }
setHeight(height) { this.height \= height; }
getOutWidth() { return this.outWidth; }
setOutWidth(outWidth) { this.outWidth \= outWidth; }
getOutHeight() { return this.outHeight; }
setOutHeight(outHeight) { this.outHeight \= outHeight; }
}
⚠️ 注意点(重要)
① 戻り値は Promise のまま
JavaScript const info \= ImageInfo.getImageInfoFromFile(path);
これは Promise なので、
JavaScript info.then(i => console.log(i));
のように使う必要があります。
② await → then の対応関係
| async/await | then |
|---|---|
const x = await A |
A.then(x => ...) |
try/catch |
.catch() |
return |
return Promise.resolve(...) or then内return |
③ null返しの扱い
JavaScript if (!input) return Promise.resolve(null);
これを書かないと 同期値とPromiseが混ざるので注意。
👍 どっちが良いか(実務視点)
正直に言うと:
async/await → 可読性が高い(推奨)
then → コールバック地獄になりやすい
ただし今回みたいに:
既存コードが then ベース
async がバグ原因(無限ループなど)
👉 こういう場合は then に統一するのはアリです
💡 追加アドバイス(今回の文脈的に重要)
あなたの過去ログを見ると:
async/awaitで無限ループになった
これはかなりの確率で👇
await漏れ
非同期関数を同期的に再帰
Promiseをreturnしてない
が原因です。
必要なら
「async版のままでバグ直す」
「全体をthenに統一」
「非同期処理の設計見直し」
このへん一緒に整理できます。
You:
async awaitの汚染を減らすには
ChatGPT:
いい質問です。これ、Node.jsで規模が大きくなるとかなり重要な設計ポイントです。
結論から言うと
「非同期を“境界に閉じ込める”」のが一番効きます。
🧠 async/await汚染の正体
一度 async を使うと:
JavaScript
function A() {
return B(); // BがasyncならPromiseになる
}
👉 呼び出し元も全部 async にしないといけない
これが「汚染」です。
🧩 対策①:非同期を“入口だけ”にする(最重要)
❌ 悪い例(汚染拡大)
JavaScript
async function parse() {
const info \= await ImageInfo.getImageInfoFromFile(path);
return process(info);
}
async function main() {
const result \= await parse();
}
✅ 良い例(境界で止める)
JavaScript
function parse(buffer) {
// ここは完全同期
return process(buffer);
}
function main() {
return ImageInfo.getImageInfoFromFile(path)
.then(buffer => parse(buffer));
}
👉 非同期は外側だけ
👉 中は全部同期関数にする
🧩 対策②:データ取得と処理を分離
これかなり効きます👇
❌ よくある構造
JavaScript
async function convert(path) {
const info \= await getImageInfo(path);
return resize(info);
}
✅ 分離する
JavaScript
function resize(info) {
return ... // 同期処理
}
function convert(path) {
return getImageInfo(path)
.then(info => resize(info));
}
👉 I/O(非同期)とロジック(同期)を分離
🧩 対策③:Promiseを“返すだけ”の関数にする
JavaScript
function getInfo(path) {
return fs.promises.readFile(path)
.then(buf => sharp(buf).metadata());
}
👉 asyncを使わないことで
👉 呼び出し側が「awaitするかthenするか」選べる
🧩 対策④:トップレベルだけ async
JavaScript
async function main() {
const info \= await getInfo(path);
console.log(info);
}
main();
👉 内部は全部thenでもOK
👉 asyncはここだけ
🧩 対策⑤:同期風APIを作る(ラッパー)
頻出パターンはこれ👇
JavaScript
function getImageInfoSafe(path) {
return getImageInfo(path).then(x => x ?? defaultValue);
}
👉 呼び出し側の分岐を減らす
👉 async汚染も減る
🧩 対策⑥:バッチ化(かなり効く)
JavaScript
Promise.all(files.map(f => getImageInfo(f)))
.then(results => {
// ここは同期っぽく処理できる
});
👉 await連鎖を減らせる
🎯 実務でのベスト構成
あなたのケース(画像処理+EPUB生成)だと👇
\[非同期層\] - ファイル読み込み - 画像メタ取得(sharp) \[同期層\] - EPUB構造生成 - HTML生成 - 文字処理
👉 ImageInfo は「非同期層の端」に置くのがベスト
🚨 よくあるNGパターン
① asyncの連鎖
JavaScript
async function A() {
return await B(); // ←無意味
}
👉 これはただの汚染
② await in ループ
JavaScript
for (const f of files) {
await process(f);
}
👉 並列にできるのに直列になる
③ Promise返してない
JavaScript
function A() {
fs.promises.readFile(...).then(...);
}
👉 呼び出し元が制御不能
💡まとめ
汚染を減らす5原則
非同期は入口・出口だけ
ロジックは全部同期関数に分離
asyncよりthenを境界で使う
Promiseをそのまま返す
並列化できるところはPromise.all
🔧 あなたのコードに対する最適解
今回の ImageInfo はかなり良い位置にいて:
👉 getImageInfo だけ非同期
👉 他は全部同期
→ すでに理想構造に近いです
もし希望あれば:
EPUB全体の非同期設計レビュー
無限ループの原因特定
sharp周りの最適化(ここ地雷多い)
このあたりかなり深く見れます。
Is this conversation helpful so far?