11. 非同期とエラー処理

Promise の型 / catchunknown である理由

📅 作成: 2026-08-29 / 更新: 2026-08-29

この章の到達点

この章の内容

  1. Promise の型
  2. async と await
  3. catch が unknown である理由
  4. 例外か、戻り値か

11.1 Promise の型

Promise<T> の T は「成功したときの値」

const p1: Promise<string> = Promise.resolve("ok");
const p2: Promise<void> = Promise.resolve();

async function load(): Promise<string> {
	return "ok";           // Promise で包まなくてよい
}

失敗したときの型は指定できません。

Promise<T> に「エラーの型」を書く場所はありません。JavaScript では何でも throw できるためです。この制約が catch の扱い(11.3)に効いてきます。

Java の throws IOException、C# のドキュメントコメントに相当するものは型としては存在しません。

async 関数の戻り値は必ず Promise

async function f(): string {
	//              ~~~~~~
	// error TS1064: The return type of an async function must be
	//               the global Promise<T> type.
	//
	// 訳: async 関数の戻り値の型は、グローバルな Promise 型である必要があります。
	return "x";
}

async function g(): Promise<string> {    // 正しい
	return "x";
}

並行実行と型

// Promise.all はタプルとして推論される
const [a, b] = await Promise.all([
	Promise.resolve("文字列"),
	Promise.resolve(42),
]);
// a は string、b は number

// allSettled は結果の union を返す
const results = await Promise.allSettled([Promise.resolve(1)]);
for (const r of results) {
	if (r.status === "fulfilled") {
		console.log(r.value);       // 判別可能ユニオン(08)
	} else {
		console.log(r.reason);
	}
}

Promise.allSettled の戻り値は判別可能ユニオンです(08)。標準ライブラリでもこの設計が使われています。

11.2 async と await

await し忘れを検出する

async function load(): Promise<string> {
	return "data";
}

async function main() {
	const s = load();               // await を忘れた
	console.log(s.toUpperCase());
	//            ~~~~~~~~~~~
	// error TS2339: Property 'toUpperCase' does not exist on type 'Promise<string>'.
}

型があると await の忘れが分かります。JavaScript では [object Promise] という文字列が出力されるまで気づけません。

forEach の中では待たれない

これは型では防げない事故です。

const ids = ["1", "2"];

// 待たれない — forEach は Promise を無視する
ids.forEach(async (id) => {
	await save(id);
});
console.log("完了");     // save が終わる前に表示される

07. 関数の型で見たとおり、void を返すコールバックには何を返してもよいためエラーになりません。for...ofPromise.all を使ってください。

// 順番に待つ
for (const id of ids) {
	await save(id);
}

// まとめて待つ
await Promise.all(ids.map((id) => save(id)));
0ms 100ms 200ms 300ms 順次: await を2回 wait(200, "A") wait(100, "B") 300ms 並行: Promise.all wait(200, "C") wait(100, "D") 200ms 互いに依存しない処理は Promise.all でまとめる。依存があるなら順次にする
図 1 — 順次と並行の所要時間の違い。待ち時間が重なるかどうかで決まる

動かしてみる

// async-order.ts
function wait(ms: number, label: string): Promise<string> {
	return new Promise((resolve) => setTimeout(() => resolve(label), ms));
}

async function main() {
	// 順番に待つ — 合計 300ms
	const a = await wait(200, "A");
	const b = await wait(100, "B");
	console.log("順次:", a, b);

	// まとめて待つ — 合計 200ms
	const [c, d] = await Promise.all([wait(200, "C"), wait(100, "D")]);
	console.log("並行:", c, d);
}

main();
$ npx tsx async-order.ts
順次: A B
並行: C D

11.3 catch が unknown である理由

何でも throw できる

JavaScript では Error 以外も投げられます。

throw "文字列";
throw 42;
throw { code: "E001" };

そのため、catch で受け取る値の型を Error と決めつけられません。

try {
	risky();
} catch (e) {
	// e の型は unknown(strict のとき)
	console.log(e.message);
	//          ~
	// error TS18046: 'e' is of type 'unknown'.
}

Java・C# から来た人へ

Java の catch (IOException e)、C# の catch (IOException e) のように型で捕まえ分けることはできません。TypeScript の catch は常に1つで、中で自分で判定します。

strict この unknownuseUnknownInCatchVariablesstrict に含まれる)の効果です。無効にすると any になり、何でも書けてしまいます03. tsconfig)。

JavaScript では何でも throw できる new Error(...) "文字列" 42 { code: "E001" } catch (e) e は unknown e instanceof Error → Error として扱う それ以外 → String(e) でログに残す 型で捕まえ分けられないため、catch の中で自分で判定する
図 2 — throw できる値に制限がないため、catch の変数は unknown になる

安全に扱う型

try {
	risky();
} catch (e) {
	if (e instanceof Error) {
		console.error(e.message, e.stack);
	} else {
		console.error("Error ではない値が投げられました:", String(e));
	}
}

独自のエラークラス

// custom-error.ts
class ApiError extends Error {
	constructor(
		public readonly status: number,
		message: string,
	) {
		super(message);
		this.name = "ApiError";
	}
}

function fetchUser(id: string): string {
	if (id === "") throw new ApiError(400, "id が空です");
	if (id === "999") throw new ApiError(404, "見つかりません");
	return `user-${id}`;
}

for (const id of ["1", "", "999"]) {
	try {
		console.log(fetchUser(id));
	} catch (e) {
		if (e instanceof ApiError) {
			console.log(`[${e.status}] ${e.message}`);
		} else if (e instanceof Error) {
			console.log(`想定外: ${e.message}`);
		} else {
			console.log(`Error ではない値: ${String(e)}`);
		}
	}
}
$ npx tsx custom-error.ts
user-1
[400] id が空です
[404] 見つかりません

Error を継承したら this.name を設定してください。

設定しないと、ログに Error: 見つかりません と出てどのエラーか分かりません。また、instanceof はクラスに対してのみ働くので、構造的型付けとは無関係に名前で区別されます。

11.4 例外か、戻り値か

2つの方針

方式書き方長所短所
例外throw new Error(...)正常系のコードが読みやすい型に現れない。呼ぶ側が気づけない
戻り値Result<T, E> を返す型に現れる。処理を強制できる毎回分岐が要る

Result 型

// result.ts
type Result<T, E = Error> =
	| { ok: true; value: T }
	| { ok: false; error: E };

function parsePort(s: string): Result<number> {
	const n = Number(s);
	if (!Number.isInteger(n)) {
		return { ok: false, error: new Error(`整数ではありません: ${s}`) };
	}
	if (n < 1 || n > 65535) {
		return { ok: false, error: new Error(`範囲外です: ${n}`) };
	}
	return { ok: true, value: n };
}

for (const s of ["3000", "abc", "99999"]) {
	const r = parsePort(s);
	if (r.ok) {
		console.log(`OK: ${r.value}`);
	} else {
		console.log(`NG: ${r.error.message}`);
	}
}
$ npx tsx result.ts
OK: 3000
NG: 整数ではありません: abc
NG: 範囲外です: 99999

Result判別可能ユニオンです(08)。r.ok を確かめると、valueerror のどちらがあるかが確定します。

使い分けの基準

状況方式理由
入力の検証(想定内の失敗)戻り値失敗が普通に起きる。呼ぶ側に対処を強制したい
設定の不備・プログラムの誤り例外回復できない。落として気づかせるべき
ライブラリの境界戻り値使う側が例外の存在に気づけない
アプリの奥深く例外すべての層で分岐すると読めなくなる

全体を Result にする必要はありません。

Result境界(外部入力・API・ファイル)で使い、内側は例外という組み合わせが実務的です。すべてを Result にすると、分岐が積み重なって読みにくくなります。

この型は A2. 型レシピ集 にも収めています。