20200921のNode.jsに関する記事は10件です。

アマゾンのセール情報からASINを取得する

アマゾンのセール情報からASINを取得

 以前とは違ってアマゾンのセール情報からASINを取得してみよう。セール情報から取得する場合はアマゾンが作ったセール用のnode番号が必要になる。

sync-requestとjsdomを使っていたが、今回はrequestとjsdomを使う。

Amazonの検索結果からasinをスクレイピング
https://qiita.com/99nyorituryo/items/c5d53a3ca8a4967b5927

ASINの取得

 例として、「秋のKADOKAWAコミック&ラノベ フェア」のアドレスからASINをとる。
https://www.amazon.co.jp/b?ie=UTF8&node=8420570051

上のページを開いてからkindle本、ページ2を選ぶ。なぜそうするかというと、ページ番号を可変させて、一括でASINを取得したいから。
 下のアドレスから、page=2を探して2の部分を入れ替えながら、requestで取得する。

https://www.amazon.co.jp/s?i=digital-text&bbn=8420570051&rh=n%3A2250738051%2Cn%3A2275265051%2Cn%3A2275277051%2Cn%3A8172192051%2Cn%3A8419007051%2Cn%3A8420502051%2Cn%3A8420569051%2Cn%3A8420570051%2Cn%3A2275256051&dc&page=2&fst=as%3Aoff&qid=1600614637&rnid=2250739051&ref=sr_pg_2

jsdomやrequestをインストールする。

npm install jsdom
npm i request

ページからjsdomでasinを配列として出力している。1ページあたり16冊まで表示する。

const request = require("request");
 const jsdom = require("jsdom");
const { JSDOM } = jsdom;
fs = require('fs');

function asinGet(html){
    const dom = new JSDOM(html);
table=dom.window.document.querySelectorAll("h2.a-size-mini > a")
asin=[]//配列宣言
for (let i = 0; i < table.length; i++) {
asin[i]=table[i].href
asin[i]=asin[i].replace( /^.+dp\/(\w+)\/.+$/g , '$1' )
}
/*
ul class="a-pagination" li class="a-disabled a-last"
*/
if(dom.window.document.querySelectorAll("ul.a-pagination > li").length !== 0){

li=dom.window.document.querySelectorAll("ul.a-pagination > li")
flag=li[li.length-1].getElementsByTagName('a').length
}
else{flag=0}
console.log(asin)
console.log(flag)

return asin
}

function sleep(second) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve()
        }, second * 1000)
    })
}

function requestPromise(param){
    return new Promise((resolve, reject)=>{
        request(param, function (error, response, body) {
            if(error){
                reject("ページを取得できませんでした");
            }else{
//                console.log(body);
                asin=asinGet(body)
                asinarray=asinarray.concat(asin)
                resolve("取得できました");
            }
        })
    })
}

(async function(){

day='秋のKADOKAWA'
flag=1
asinarray=[]
filename=day+'k.json'
//https://www.amazon.co.jp/s?rh=n%3A2250738051%2Cn%3A%212275265051%2Cn%3A%212275277051%2Cn%3A%218172192051%2Cn%3A%218187544051%2Cn%3A%218187545051%2Cn%3A%218187561051%2Cn%3A8187562051&page=2&qid=1594419906&ref=lp_8187562051_pg_2

//&price=100-10000
//'+i+'
loop1:for (let i =1; i <=500;  i ++) {

var url='https://www.amazon.co.jp/s?i=digital-text&bbn=8420570051&rh=n%3A2250738051%2Cn%3A2275265051%2Cn%3A2275277051%2Cn%3A8172192051%2Cn%3A8419007051%2Cn%3A8420502051%2Cn%3A8420569051%2Cn%3A8420570051%2Cn%3A2275256051&dc&page='+i+'&fst=as%3Aoff&qid=1600614637&rnid=2250739051&ref=sr_pg_2'
if(flag==0){break loop1;}
 await sleep(3) 
await requestPromise(url);
console.log(i)
  }

  console.log(asinarray)
  if(asinarray.length==0){

  }else{
  console.log('ファイルを書き込みます')
fs.writeFileSync(__dirname + '/json/kindle_sale/asin/'+filename, JSON.stringify(asinarray, null, 1),'utf-8')

}
}
)();
console.log("リクエスト開始");

 取得したASINは次のリンクで説明するようにPA-APIでアフィリエイトサイトの情報やリンクを生成する。

PA-API v5でAmazonの商品情報を取得する
https://qiita.com/99nyorituryo/items/e337e6a75c361521f297

このASINを利用して実際のセールのページを作りました。
https://kyukyunyorituryo.github.io/kindle_sale/

参考
https://qiita.com/KuwaK/items/6455e34c245992a73aa1

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 周りの知識を整理したい

かれこれTypeScript+Jest案件で1週間以上ハマっている。それはおそらく各技術をちゃんと理解せず、雰囲気だけで使っているから。
なので私が今良くわかっていない以下の6つの技術に関して、自分なりに整理してみようと思います。

  • JavaScript
  • 以下予定
    • TypeScript
    • webpack
    • Node.js
    • Jest
    • babel

JavaScript(ECMAScript)

私のよく知ってるJavascriptは、ブラウザで動くやつです。index.htmlに直接書いたり、外部ファイルに記述してscriptタグで読み込んだりするやつです。これについて調べてみます。
もともとは1995年に"make web pages alive"というスローガンのもとに作られ、その目論見は見事成功しているように思えます。もともとはLiveScriptという名前だったようです。それが、当時流行っていたJavaに乗っかる形でJavaScriptと改名したとのことで、Javaと全く無関係ではないようです。しかし1997年にはECMAScriptという名前になり汎用言語としての規格が制定され、JavaScriptはその実装の一つとして独自の進化を遂げ、Javaとは何ら関係なくなりました。

ECMAScriptのバージョンについて

2015年以降は、ECMAScript2015 など、西暦に基づいた名前になっています。以下、主なECMAScriptのバージョンについて書いてみます。

ECMAScript1 (1997)

最初のエディションです。

ECMAScript3 (1999)

  • 正規表現
  • try/catch

が追加されました。ES3は全てのブラウザがサポートする最も高いバージョンです。 トランスパイラのターゲット言語として指定されているのを見たことがありますが、そういうことだったんですね。

ECMAScript5 (2009)

いわゆるES5です。ES5から追加された機能としては、

  • "use strict"
  • Array.isArrayArray.forEach などの配列周りの関数
  • JSON.parse
  • Date.now()
  • getterとsetter
  • 配列の最後の要素の後の余分なカンマ(←地味に嬉しい)

などがあるようです。だいぶモダンみが増してきました。ES5は全てのモダンなブラウザがサポートする最も高いバージョンです。というのは、IE9が"use strict"に対応していない、というのがあるみたいです。

ECMAScript6 (2015)

いわゆるES6ですね。このバージョンから、バージョン番号は西暦の下1桁+1になっててややこしいです。

  • letconst
  • Promise
  • Arrow Functions (() => {}みたいなやつ)
  • Class
  • デフォルトパラメータ

などが追加され、より関数型言語を意識した形になっています。個人的にはES5->ES6の変化が最も大きく感じます。IE以外は全てサポートしているようです。

ECMAScript7 (2016)

  • ** (exponential operator)

など。ES7をフルサポートするのは Chrome と Opera のみのようです。

ECMAScript8 (2017)

  • async/await

構文糖衣ではありますが、これがないと生きていけない人もいるのではないでしょうか。

ECMAScript9 (2018)

  • rest / spread properties
    • 多値をシンプルに受け渡しできる構文。便利。

環境

ECMAScriptそのものは、I/OやDOMについての規定はないようです。そのへんはJavaScriptにおいて規定されているという認識です。また、JavaScriptといってもブラウザとNode.jsでは環境が異なるはずです。そのへんはまた後で詳しく調べる。

JavaScript Engine

一口にブラウザのJavaScriptといっても、その実装はブラウザによってまちまちのようです。

  • Chorome: V8
  • FireFox: SpiderMonkey
  • IE: Chakra

これらが共通でフルサポートしているのがES5で、後述するトランスパイラはより上位のECMAScriptのバージョンをES5まで落とすことでマルチブラウザ対応を可能にします。

Transpiling

先述したES6以降に備わっている素敵な機能は、全ての環境、ブラウザで使えるわけではありません。では単なる絵に描いた餅かというとそうではなく、ES2015以降で頻繁に行われるようになったトランスパイラなるものを使い、ES3などのどのブラウザでもサポートされているようなバージョンでも動くソースに変換します。
コンパイルは高級な言語からより低級な言語に変換する処理と認識していますが、トランスパイルは、同レベルの別の言語、または同一言語のより下位のバージョンに変換する処理といったところでしょうか。コンパイラはないと困るが、トランスパイラはより幸せになるためのもので、必要性はコンパイラほどではないイメージです。機械語を直で書く人もいるようですが…。
バージョン間の差異を埋めるのにPolyfillという手法もあるようですが、これは実行時にAPIなどの機能を補完するもので、根本的な文法の違いを吸収することはできません。いっそソースを書き換えてしまえというのがTranspilingの考え方かと思います。
例えばトランスパイラは、以下のものをES5やES3に変換します

  • TypeScript
  • CoffeeScript
  • ES2015

ではトランスパイラにはどういうものがあるのかといえば、多分色々あるんでしょうけど、ここでは私の関心のあるところの Babel に限定していろいろ調べてみます。

Babel

Babelのページに行ったら、Babelはコンパイラです、とのことでした。コンパイラはトランスパイラも含むってことですかね。

Plugin

Babelそのものは、プラグインなしでは何もしないようです。全ての変換の仕事はプラグインを追加して初めて行われるようです。No plugin, No transpile です。
Pluginには、Transform PluginsとSyntax Pluginsがあるようです。

https://babeljs.io/docs/en/plugins

Transform Plugins

コードの変換をします。arrow-functions や block-scoping, for-of など、機能ごとに細かく分かれているのがわかります。
ES2015対応などの主だった機能の他、モジュール作法の吸収、実験的な機能、ミニフィケーションなどもTransform Plugins に含まれています。React関連のプラグインがあるのが不思議です。Reactそのものは言語ではないはずですが…。この辺は余裕があれば掘り下げたい。
Tranform Plugins は関連する Syntax Plugin を読み込むので、両方指定する必要はないとのこと。
これらのいわば"部品"を必要に応じて個々に組み合わせることもできるのでしょうけど、我々はそんなことした覚えはありません。普通は後述する Preset を使います。

Syntax Plugins

Syntax Plugins はコードの変換をしないそうです。じゃあ何をするの?→パースをします。と言われてもピンとこない。Syntax Plugins はコードをASTに変換して、Tranform Plugins はASTを操作するのだろうか。この辺は私の関心と外れるので深追いはしない。

Presets

プラグインおまかせ詰め合わせセットだと思います。@babel/preset-envとかいうやつですね。調べていくと、どうやらただの詰め合わせではないようです(そっちの方が話は単純なのですが…)

@babel/preset-env

よく見るこの人について少し調べてみました。Presets は、オプションを取ることができます。例えばこの人に関して言えば、targets というオプションによって、ターゲットとなるブラウザを絞ることができるようです(知らなかった)。それによる効果はよくわかりませんが、大事なのは、オプションによってプラグイン(詰め合わせ)の内容が変化するということですかね。しかしPresets の挙動を変える要因はこれだけではありません。
このPreset においては、targets が指定されていないときに、browserslist というホストの環境をチェックし、挙動を変えるようなのです。ここからわかることは、Babel の挙動は、Babelの設定ファイルだけでは決まらないということです。JavaScriptエコシステムたちの設定ファイルの肥大化と偏在化、環境への依存の不透明度が全貌の把握を困難にしているというのが問題だと思っているので、これも元凶の一つかなという感じです。

Configuration

Babelの設定ファイルですが、

  • node_modulesをコンパイルするとき
    • babel.config.json
    • package.jsonbabelセクション
  • プロジェクトの一部(のディレクトリ?)に適用したいとき
    • .babelrc.json

などがあるようです。
これらのファイルは.jsonのかわりに.js.mjs.cjsとして動的に構成することもできる他、.babelrc.babelrc.jsonの代わりになるというルールもあります。
これらの使い方でけっこうハマりどころがありそうなので以下のリンクをあとで読む(よくわらからずに使っているbabel-jestについても書いてある…)
https://babeljs.io/docs/en/config-files

疲れたので今日はここまで。
TODO: 続きを書く。

参考

https://www.w3schools.com/js/js_versions.asp
https://javascript.info/intro
https://en.wikipedia.org/wiki/ECMAScript

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeORMが構文エラーで動かない問題を解決した

環境

環境 バージョン
node 12.18.3
typescript 3.8.3
typeorm 0.2.26

問題と解決策

  • 問題

    • typeorm init --name MyProject --database mysqlで作成したプロジェクトをビルドして動かそうとするとSyntaxError: Cannot use import statement outside a moduleとエラーになる
  • 原因

    • ジェネレーターが吐き出すコードがおかしいせい
    • 具体的にはビルド後に.tsファイルを参照する状態になっている
      • 動くわけがない
  • 解決策

    • ormconfig.jsonentitiessubscribersのセクションを以下のように出力先の.jsファイルを参照する様に書き換える
  "entities": [
    "dist/entity/**/*.js"
  ],
  "migrations": [
    "dist/migration/**/*.js"
  ],
  "subscribers": [
    "dist/subscriber/**/*.js"
  ],
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsでsubmitボタンをFont Awswsomeにする方法

プログラミング勉強日記

2020年9月21日
Node.jsのejsファイルでformでデータを送るときにFont Awesomeを使おうとするときに試行錯誤したので備忘録として書いておく。

現在と目標

 現在は以下のように「削除」ボタンになっている。
image.png

現在
<form action="/delete/<%= user.id %>/<%= users[0].id %>" method="post">
  <input type="submit" value="削除">
</form>

 それを以下のようにFont Awesomeを用いてアイコンにしたい。
image.png

解決方法

解決方法
<form action="/delete/<%= user.id %>/<%= users[0].id %>" method="post">
  <button type="submit" class="btn"><i class="fas fa-trash-alt"></i></button>
</form>

 個人的にはアイコンの周りの線(ボタンの部分)を消したいが、うまくできないのでこれがうまくできたらまた記事にしようと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsでsubmitボタンをFont Awesomeにする方法

プログラミング勉強日記

2020年9月21日
Node.jsのejsファイルでformでデータを送るときにFont Awesomeを使おうとするときに試行錯誤したので備忘録として書いておく。

現在と目標

 現在は以下のように「削除」ボタンになっている。
image.png

現在
<form action="/delete/<%= user.id %>/<%= users[0].id %>" method="post">
  <input type="submit" value="削除">
</form>

 それを以下のようにFont Awesomeを用いてアイコンにしたい。
image.png

解決方法

解決方法
<form action="/delete/<%= user.id %>/<%= users[0].id %>" method="post">
  <button type="submit" class="btn"><i class="fas fa-trash-alt"></i></button>
</form>

 個人的にはアイコンの周りの線(ボタンの部分)を消したいが、うまくできないのでこれがうまくできたらまた記事にしようと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript エラー処理 完全ガイド

image.png

本記事は、Valentino Gagliardi 氏の "A mostly complete guide to error handling in JavaScript." を許可を頂いた上で翻訳したものです。

TOC

プログラミングにおけるエラーとは?

私たちの書くプログラムは 常にうまく動作するわけではありません。

時に、プログラムを停止させたり、ユーザーに何か問題が起こったことを知らせたい シチュエーションがあります。

例えば、以下のようなケースがあるでしょう:

  • プログラムが存在しないファイルを開こうとした
  • ネットワークの接続が不調である
  • ユーザーが無効な値を入力した

すべてのケースで、私たちがプログラマーとして、またはプログラミングエンジンを通して、 エラー を作成します。

エラーを作成することで、ユーザーに問題が起きたことをメッセージで伝えたり、プログラムの実行を停止させたりできるのです。

JavaScript におけるエラーとは?

JavaScript におけるエラーはオブジェクト です。このオブジェクトは、後にプログラムを停止するために 投げられる ものです。

JavaScript で新しくエラーを作成するには、適切な コンストラクタ関数 を呼び出します。例えば、一般的なエラーを新規に作成するには以下を実行します:

const err = new Error("Something bad happened!");

new というキーワードを省略することもできます:

const err = Error("Something bad happened!");

一度作成されると、エラーオブジェクトは3つのプロパティを提供します。

  • message: エラーメッセージを含む文字列
  • name: エラーのタイプ
  • stack: 関数実行のスタックトレース

例えば、適当なメッセージ文字列でTypeError オブジェクトを作成した場合、message は実際に渡した文字列となり、name"TypeError"となります:

const wrongType = TypeError("Wrong type given, expected number");

wrongType.message; // "Wrong type given, expected number"
wrongType.name; // "TypeError"

Firefox は上記のプロパティの他に、columnNumberfilenamelineNumberといった非標準プロパティを実装しています。

JavaScript エラー型の種類

JavaScript にはたくさんのエラー型があります。具体的には以下の通りです:

  • Error
  • EvalError
  • InternalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

これらのエラー型は、あたらしいエラーオブジェクトを返す 本物のコンストラクタ関数 であることを忘れないでください。

あなた自身のエラーオブジェクトを作成する際、ErrorTypeError という最も一般的な 2 つのエラー型を使うことが多いでしょう。

エラーの大多数は InternalErrorSyntaxError のように、JavaScript エンジンから直接的に発現するものがほとんどです。

TypeError の一例は、const に再代入しようとした際に発生します:

const name = "Jules";
name = "Caty";

// TypeError: Assignment to constant variable.

SyntaxError の一例は、タイプミスをしたときに発生します:

va x = '33';
// SyntaxError: Unexpected identifier

または、awaitasync 関数以外で利用するなど、予約語を不適切な場所を使った場合にも発生します:

function wrong(){
    await 99;
}

wrong();

// SyntaxError: await is only valid in async function

TypeErrorの他の例としては、ページに存在しない HTML 要素を指定したときに発生します:

Uncaught TypeError: button is null

これらのよくあるエラーオブジェクトに加えて、AggregateError オブジェクトが JavaScript にもうすぐ導入される予定です。後ほど見るように、AggregateError は複数のエラーをまとめる際に便利です。

これらの組み込みエラーに加えて、ブラウザでは以下のようなもの目にすることがあります:

  • DOMException
  • DOMError (Dupulicated, 今は使われていない)

DOMException は Web APIs に関連するエラーファミリーです。ブラウザの中で、ばかげたことをしたときに投げられます。例えば以下のようなことです:

document.body.appendChild(document.cloneNode(true));

結果:

Uncaught DOMException: Node.appendChild: May not add a Document as a child

完全なリストは、MDNのこちらのページを参照してください。

例外とは?

多くのデベロッパーは、エラーと例外を同様のものとして考えています。実際には、 エラーオブジェクトが投げられたときにのみ、エラーオブジェクトが例外になる のです。

JavaScript で例外を投げるには、throwとエラーオブジェクトを用います:

const wrongType = TypeError("Wrong type given, expected number");

throw wrongType;

短縮形のほうがより一般的です。多くのコードベースで以下のようなものを目にするでしょう:

throw TypeError("Wrong type given, expected number");

または

throw new TypeError("Wrong type given, expected number");

関数や条件分岐構文の外で例外を投げることはほとんどありません。代わりに、以下の例を考えてみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

ここでは、関数の引数が文字列(string)かどうかをチェックしています。文字列でなければ、例外を投げます。

JavaScript のルール的には、エラーオブジェクトだけではなく何でも投げることができます:

throw Symbol();
throw 33;
throw "Error!";
throw null;

しかしながら、 プリミティブ型を投げることは避け、適切はエラーオブジェクトを投げるべき です。

そうすることで、コードベースにおいてエラー処理の一貫性を保つことができます。他のチームメンバーがエラーオブジェクトにおいて error.messageerror.stack にアクセスすることができます。

例外を投げると何が起きる?

例外はエレベーターが上に行くようなものです。 一度例外を投げると、どこかで止められない限りプログラムスタックの中でぶくぶくと泡立ってしまします。

以下のようなコードを考えてみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase(4);

このコードをブラウザもしくは Node.js で実行した場合、プログラムは停止し以下のようなエラーを表示します:

Uncaught TypeError: Wrong type given, expected a string
    toUppercase http://localhost:5000/index.js:3
    <anonymous> http://localhost:5000/index.js:9

さらに、エラーが発生した正確な行数を把握することができます。

この表示が スタックトレース であり、プログラムの問題を追跡する際に便利です。

スタックトレースは下から上に積み上がります。つまりここでは以下のようになっていました:

    toUppercase http://localhost:5000/index.js:3
    <anonymous> http://localhost:5000/index.js:9

ここから以下のことが言えます:

  • 9 行目にあるプログラムの何かが toUppercase を呼び出した
  • 3 行目において toUppercase で問題が発生した

ブラウザのコンソールで確認する以外にも、エラーオブジェクトの stack プロパティにアクセスすることによってスタックトレースを見ることができます。

もし例外が キャッチされなかった 場合、つまり、プログラマが例外をキャッチするために何もしなかった場合、プログラムはクラッシュします。

コードの中で、いつ、どこで例外をキャッチするかは、その時々で異なります。

例えば、 プログラムを完全にクラッシュさせるために、例外をスタックに加えて伝播させたいかもしれません。 これは、無効なデータで処理を進めるよりもプログラムを停止させたほうが安全である、といった、致命的なエラーを処理する際に起こりうることです。

さて、ここまでで基本の紹介をしたので、 JavaScript の同期処理と非同期処理における、エラーと例外処理 に話を進めましょう。

同期的エラー処理

同期処理のコードはほとんどの場合単純でわかりやすいので、エラー処理も簡単です。

通常関数のエラー処理

同期処理のコードは、書かれた通りに順番に実行されます。前述のコードをもう一度見てみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase(4);

ここで、JavaScript エンジンは toUppercase を呼び出して実行しています。すべての処理は 同期的 に行われます。このように同期関数から発生する例外を キャッチ するには、try/catch/finally を使うことができます:

try {
  toUppercase(4);
} catch (error) {
  console.error(error.message);
  // or log remotely
} finally {
  // clean up
}

通常、try はハッピーパスや、潜在的に例外を投げる可能性のある関数呼び出しに対して利用します。

catch は、 実際に例外を捉えます。 エラーオブジェクトを受け取り 、エラーの内容を検査することができます(そして本番環境ではログをリモートサーバーに送信したりします)。

一方で、finally ステートメントは、関数の実行結果に関わらず実行されます。つまり、関数が失敗したか成功したかにかかわらず finally 内に書かれたコードは実行されます。

try/catch/finally同期的な 構造であることを覚えておいて下さい。そしていま、 非同期処理のコードから発生する例外をキャッチする方法を獲得した のです。

ジェネレーター関数のエラー処理

JavaScript におけるジェネレーター関数は、関数の特殊な形式です。

この形式の関数は、関数の内側のスコープとその外側の間で 双方向のコミュニケーションチャネル を提供する以外に、 任意に停止したり再開したり することができます。

ジェネレーター関数を作成するには、function キーワードの後ろにアスタリスク * を付けます:

function* generate() {
//
}

そうすると、値を返すために関数内で yield を使用することができます:

function* generate() {
  yield 33;
  yield 99;
}

ジェネレーター関数の返り値イテレータオブジェクト です。ジェネレーターから値を取り出すためには、2つの方法があります:

  • イテレータオブジェクトの next() を呼び出す
  • for...ofイテレーション する

先程の例で、ジェネレーターから値を取り出す場合は、以下のようにできます:

function* generate() {
  yield 33;
  yield 99;
}

const go = generate();

ここで go がイテレータオブジェクトになります。

ここから、go.next() を呼び出し、実行を進めることができます:

function* generate() {
  yield 33;
  yield 99;
}

const go = generate();

const firstStep = go.next().value; // 33
const secondStep = go.next().value; // 99

ジェネレーターは、 呼び出し元から値や例外を受け取ることもできます。

next() に加えて、ジェネレーターから返されたイテレータオブジェクトは、throw() メソッドを持っています。

このメソッドを利用して、ジェネレーターに例外を注入することによってプログラムを停止させてみましょう:

function* generate() {
  yield 33;
  yield 99;
}

const go = generate();

const firstStep = go.next().value; // 33

go.throw(Error("Tired of iterating!"));

const secondStep = go.next().value; // never reached

注入された例外をキャッチするには、ジェネレーター関数内の処理を try/catch 構文で囲む必要があります(必要であれば finally も利用できます):

function* generate() {
  try {
    yield 33;
    yield 99;
  } catch (error) {
    console.error(error.message);
  }
}

ジェネレーター関数は例外を関数の外に投げることもできます。この仕組みは、try/catch/finally を使って同期処理の例外をキャッチするものと同じです。

ジェネレーター関数に対して for...of 構文を利用する例は以下のとおりです:

function* generate() {
  yield 33;
  yield 99;
  throw Error("Tired of iterating!");
}

try {
  for (const value of generate()) {
    console.log(value);
  }
} catch (error) {
  console.error(error.message);
}

/* Output:
33
99
Tired of iterating!
*/

ここでは、try ブロックの中でハッピーパスを実行し、例外があれば catch でキャッチします。

非同期エラー処理

JavaScript はシングルスレッドで実行されるプログラム言語であり、原理的には同期的です。

ブラウザエンジンのようなホスト環境が JavaScript の機能を拡張させたことで、外部のシステムと通信したり、I/O 処理を行うための、たくさんの Web API が使えるようになりました。

ブラウザにおける非同期性の例は タイムアウト(timeouts)、イベント(events)、プロミス(Promise) があります。

非同期の世界におけるエラー処理 は同期の世界におけるそれとは異なります。

いくつか例を見ていきましょう。

タイマーのエラー処理

JavaScript を学び始めたばかりのとき、try/catch/finally 構文について学ぶと、あらゆるコードブロックを try/catch/finally 構文 で囲みたくなるかもしれません。

例えば以下のような関数を考えてみましょう:

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000)
}

この関数は、約 1 秒後にエラーを投げます。この例外を正しく扱うにはどうしたらよいでしょうか?

以下のようなコードは 上手く動きません :

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000);
}

try {
  failAfterOneSecond();
} catch (error) {
  console.error(error.message);
}

前述したように、try/catch 構文は同期的です。一方で、ここでは setTiemout という、タイマー機能を持つブラウザの API を利用しています。

setTimeout に渡したコールバック関数が実行されるときには、既にtry/catch 構文の実行は 終わっている のです。上のプログラムは例外をキャッチすることができず、クラッシュしてしまいます。

2 つの異なったトラックが実行されているのです:

Track A: --> try/catch
Track B: --> setTimeout --> callback --> throw

プログラムをクラッシュさせたくなければ、try/catch 構文を、setTimeout に渡しているコールバック関数の中に移動する必要があります。

しかし、このアプローチは多くの場合意味を成しません。後で見るように、 Promises を用いた非同期エラー処理がより優れている のです。

イベントのエラー処理

Document Object Model (DOM) の HTML ノードは、EventTarget と連携しています。EventTarget は、ブラウザにおけるあらゆるイベントエミッターの共通の祖先といえる存在です。

これはつまり、ページ上の全ての HTML 要素におけるイベントを取得することができることを意味します。

(Node.js も今後のリリースで EventTarget をサポートする予定です)

DOM イベントに対するエラー処理の仕組みは、非同期 Web API における仕組みと同様 です。

以下の例を考えてみましょう:

const button = document.querySelector("button");

button.addEventListener("click", function() {
  throw Error("Can't touch this button!");
});

ここでは、ボタンがクリックされた瞬間に例外を投げています。どのようにその例外をキャッチするのでしょうか?以下のパターンは 上手く動作せず 、プログラムはクラッシュしてしまいます:

const button = document.querySelector("button");

try {
  button.addEventListener("click", function() {
    throw Error("Can't touch this button!");
  });
} catch (error) {
  console.error(error.message);
}

setTimeout の例で見たように、addEventListener に渡されるあらゆるコールバック関数は、非同期的に実行されます:

Track A: --> try/catch
Track B: --> addEventListener --> callback --> throw

プログラムをクラッシュさせたくなければ、addEventListener のコールバック関数内部に try/catch 構文を移動する必要があります。

しかしここでも、そのようにする意味がほぼありません。

setTimeout の例で見たように、非同期処理コードの実行パスにおいて投げられた例外は 外側でキャッチすることができるものではなく 、結果としてプログラムはクラッシュします。

次のセクションで、Promises と async/await がどのように非同期処理におけるエラー処理を手軽なものにするか見ていきます。

onerror はどうだろう?

HTML 要素には、onlickonmouseenteronchange など多くのイベントハンドラがあります。

そのなかには、onerror もありますが、throw やその類のものとは何も関係がありません。

onerror イベントハンドラは、<img><script> のような HTML 要素が存在しないリソースを扱ったときにトリガーされます。

以下のような例を考えてみましょう:

// omitted
<body>
<img src="nowhere-to-be-found.png" alt="So empty!">
</body>
// omitted

上記のような、存在しないリソースを参照する要素を含んだ HTML ドキュメントをブラウザで見ると、コンソールに以下のようなエラーが表示されます:

GET http://localhost:5000/nowhere-to-be-found.png
[HTTP/1.1 404 Not Found 3ms]

JavaScript では、このエラーを以下のように「キャッチ」できるかもしれません:

const image = document.querySelector("img");

image.onerror = function(event) {
  console.log(event);
};

より優れた形で書くと、以下のようになります:

const image = document.querySelector("img");

image.addEventListener("error", function(event) {
  console.log(event);
});

このパターンは、画像やスクリプトなどのリソースに欠損があった際に、代替となるリソースをローディングしたい場合 に便利です。

だたし、onerrorthrowtry/catch とは何の関係もないことを覚えておいて下さい。

Promise を用いたエラー処理

Promise によるエラー処理を説明するために、何度も登場している以下の例を「約束化(promisify)」させてみましょう。以下のコード例を編集していきます:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase(4);

単純に文字列もしくは例外を返す代わりに、成功とエラーを処理するための Promise.rejectPromise.resolve を利用してみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    return Promise.reject(TypeError("Wrong type given, expected a string"));
  }

  const result = string.toUpperCase();

  return Promise.resolve(result);
}

(厳密には、上記コードに非同期処理を行う部分はありませんが、説明するには十分です)

いま、toUppercase 関数は「約束」され、処理結果を扱うために then を、 リジェクトされた Promise を処理するためcatch を使うことができます:

toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message));

上記のコードは、以下のようなログを吐き出します:

Wrong type given, expected a string

Promise において、catch はエラーを処理するための構成要素です。

catchthen に加え、finally もあります。この finally は、try/catch 構文における finally と似たものです。

Promise における finally も、返された Promise の結果に 関わらず 実行されます:

toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message))
  .finally(() => console.log("Run baby, run"));

then/catch/finally に渡されたコールバック関数は、Microtask キューによって非同期に処理されることを覚えておいて下さい。これらは、イベントやタイマーよりも優先される micro task です。

プロミス(Promise)、エラー(error)そしてスロー(throw)

Promise をリジェクトする際は、引数としてエラーオブジェクト渡すのが ベストプラクティス です:

Promise.reject(TypeError("Wrong type given, expected a string"));

そうすることで、エラー処理の一貫性を保つことができます。他のチームメンバーが常に error.message にアクセスすることができますし、さらに重要なことに、スタックトレースを調査することができます。

Promise.reject に加えて、例外を投げることで Promise チェーンから抜け出すことができます。

以下のコード例を考えてみます:

Promise.resolve("A string").then(value => {
  if (typeof value === "string") {
    throw TypeError("Expected a number!");
  }
});

文字列を返すとともに Promise をリゾルブし、そしてその直後に throw によって例外を投げています。

例外の伝播を食い止めるために、通常通り catch を使うことができます:

Promise.resolve("A string")
  .then(value => {
    if (typeof value === "string") {
      throw TypeError("Expected a number!");
    }
  })
  .catch(reason => console.log(reason.message));

このパターンは、fetch を使う際によく用いられます。レスポンスオブジェクトのエラーチェックを行う例は以下の通りです:

fetch("https://example-dev/api/")
  .then(response => {
    if (!response.ok) {
      throw Error(response.statusText);
    }

    return response.json();
  })
  .then(json => console.log(json));

ここでも、catch によって例外を受け取ることができます。もし例外を受け取ることに失敗した場合、あるいはあえて受け取らないことにした場合、 例外はキャッチされるまでスタックに残り続けます。

これは一概に悪いこととは言えませんが、環境によって、キャッチされていないリジェクトに対する挙動は異なります。

例えば、Node.js は将来的に、処理されていない Promise のリジェクトがあった場合は、プログラムをクラッシュさせる予定です:

DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

必ずリジェクトはキャッチしましょう!

"プロミス化"されたタイマーのエラー処理

タイマーとイベントにおいて、コールバック関数内で投げられた例外をキャッチすることは不可能ではありません。前のセクションで、以下のような例を挙げました:

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000);
}

// DOES NOT WORK
try {
  failAfterOneSecond();
} catch (error) {
  console.error(error.message);
}

Promise によって与えられた解決策は、コードの「プロミス化」です。基本的に、Promise でタイマーを囲みます:

function failAfterOneSecond() {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(Error("Something went wrong!"));
    }, 1000);
  });
}

reject によって Promise のリジェクトをセットし、エラーオブジェクトを渡します。

この時点で、catch をつかって例外を処理することができます:

failAfterOneSecond().catch(reason => console.error(reason.message));

Tips: 成功した Promise の返り値の変数名として value、Promise のリジェクトの変数名として reason を使うことが一般的です。

Node.js は promisify と呼ばれる、古い形で書かれたコールバック API をプロミス化するユーティリティを提供しています。

Promise.all のエラー処理

Promise の static メソッドである Promise.all は Promise の配列を引数にとり、リゾルブした Promise の配列を返します。

const promise1 = Promise.resolve("All good!");
const promise2 = Promise.resolve("All good here too!");

Promise.all([promise1, promise2]).then((results) => console.log(results));

// [ 'All good!', 'All good here too!' ]

渡した配列のどれか1つでもリジェクトされた場合、Promise.all は最初にリジェクトされた Promise のエラーとともにリジェクトします。

このような状況を扱うために、前のセクションで見たように catch が利用できます:

const promise1 = Promise.resolve("All good!");
const promise2 = Promise.reject(Error("No good, sorry!"));
const promise3 = Promise.reject(Error("Bad day ..."));

Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error.message));

Promise.all の実行結果に関わらず関数を実行するには、finally を利用します:

Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error.message))
  .finally(() => console.log("Always runs!"));

Promise.any のエラー処理

Promise.any (Firefox > 79, Chrome > 85) は、Promise.all の反対の処理をする関数と考えることができます。

Promise.all が、渡した配列の中に 1 つでもリジェクトされるものがあった場合にエラーを返すのに対し、Promise.any はリジェクトが発生しても、リゾルブしたものが 1 つでもあればそれを返します。

Promise.any に渡した配列に含まれる すべての Promise がリジェクトされた場合、結果として得られるエラーは AggregatedError です。以下のようなコード例を考えてみましょう:

const promise1 = Promise.reject(Error("No good, sorry!"));
const promise2 = Promise.reject(Error("Bad day ..."));

Promise.any([promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log("Always runs!"));

catch を使ってエラーを処理しています。このコードの実行結果は以下の通りです:

AggregateError: No Promise in Promise.any was resolved
Always runs!

AggregatedError オブジェクトは、通常の Error オブジェクトと同様のプロパティに加えて、errors プロパティを持っています:

//
  .catch(error => console.error(error.errors))
//

このプロパティは、それぞれのリジェクトで返されたエラーの配列を格納しています:

[Error: "No good, sorry!, Error: "Bad day ..."]

Promise.race のエラー処理

Promise.race は、Promsie の配列を引数に取ります:

const promise1 = Promise.resolve("The first!");
const promise2 = Promise.resolve("The second!");

Promise.race([promise1, promise2]).then(result => console.log(result));

// The first!

得られる返り値は、 「レース」を制した 1 番着の Promise です。

リジェクトされた場合はどうなるのでしょうか?リジェクトされる Promise が一番でなければ、Promise.race はリゾルブします:

const promise1 = Promise.resolve("The first!");
const rejection = Promise.reject(Error("Ouch!"));
const promise2 = Promise.resolve("The second!");

Promise.race([promise1, rejection, promise2]).then(result =>
  console.log(result)
);

// The first!

もし リジェクトが一番になった場合、Promise.race はリジェクトされ 、以下のようにしてリジェクトをキャッチすることができます:

const promise1 = Promise.resolve("The first!");
const rejection = Promise.reject(Error("Ouch!"));
const promise2 = Promise.resolve("The second!");

Promise.race([rejection, promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error.message));

// Ouch!

Promise.allSettled のエラー処理

Promise.allSettled は ECMAScript 2020 で追加される関数です。

この関数を使って処理するケースはそれほど多くありません。なぜなら、 Promise のリジェクトがあったとしても、返り値は常にリゾルブされた Promise になるため です。

以下のようなコード例を考えてみます:

const promise1 = Promise.resolve("Good!");
const promise2 = Promise.reject(Error("No good, sorry!"));

Promise.allSettled([promise1, promise2])
  .then(results => console.log(results))
  .catch(error => console.error(error))
  .finally(() => console.log("Always runs!"));

上の例では、リゾルブする Promise とリジェクトされる Promise を 1 つずつ含め、配列として渡しています。

このケースでは、catch が実行されることはありません。代わりに finally が実行されます。

then 内の処理によってロギングされる結果は次の通りです:

[
  { status: 'fulfilled', value: 'Good!' },
  {
    status: 'rejected',
    reason: Error: No good, sorry!
  }
]

async/await のエラー処理

JavaScript の async/await は非同期関数を表しますが、コードを読む立場からみれば、同期関数の 可読性の高さ の恩恵を受けているといえます。

話を単純にするために、何度も登場している同期関数 toUppercase を、function キーワードの前に async を付け足すことで、非同期関数に変換します:

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

async というプレフィックスを使うことで、関数に Promise を返す ように仕向けることができるようになります。これはつまり、thencatchfinally といったチェーンが使えることを意味します:

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase("abc")
  .then(result => console.log(result))
  .catch(error => console.error(error.message))
  .finally(() => console.log("Always runs!"));

async 関数内で例外を投げた 場合、この例外は、 裏側で機能している Promise をリジェクト させます。

どんなエラーも、catch によってキャッチすることができます。

最も重要なことは、このスタイルに加えて同期関数と同様に try/catch/finally 構文を使える、ということです。

以下の例では、toUppercase 関数を consumer という他の関数から呼び出しています。consumer 内部では、toUppercase 関数を try/catch/finally 関数で囲っています:

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

async function consumer() {
  try {
    await toUppercase(98);
  } catch (error) {
    console.error(error.message);
  } finally {
    console.log("Always runs!");
  }
}

consumer(); // Returning Promise ignored

実行結果は以下の通りです:

Wrong type given, expected a string
Always runs!

同様のトピックは次の記事でも扱っています: How to Throw Errors From Async Functions in JavaScript?

非同期ジェネレーターのエラー処理

JavaScript の 非同期ジェネレーター は、通常の値の代わりに Promise を yeild することができるジェネレーター関数 です。

async とジェネレーター関数を組み合わせて使います。イテレータオブジェクトが呼び出し元に対して Promise を返すジェネレーター関数です。

非同期ジェネレーターを作るために、async でプレフィックスした、* を持つ関数を定義します:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}

Promise の仕組みに基づいているため、エラー処理に対しても同様のルールが適用されます。非同期ジェネレーター関数内の throw は Promise のリジェクトに繋がり、catch でキャッチすることができます。

非同期ジェネレーター関数から Promise を取り出す には、以下の 2 つのアプローチがあります。

  • thenハンドラ
  • 非同期イテレーション

上のコード例では、最初の 2 つの値が yield されたあとに、例外が投げられます。これは以下のようにできることを意味します:

const go = asyncGenerator();

go.next().then(value => console.log(value));
go.next().then(value => console.log(value));
go.next().catch(reason => console.error(reason.message));

上記コードの実行結果は以下の通りです:

{ value: 33, done: false }
{ value: 99, done: false }
Something went wrong!

もう1つのアプローチは、 for await...of非同期イテレーション を用いる方法です。非同期イテレーションを用いるためには、呼び出し側の関数を async で囲む必要があります。

以下が完全なコード例です:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}

async function consumer() {
  for await (const value of asyncGenerator()) {
    console.log(value);
  }
}

consumer();

async/await で見たように、潜在的に存在する例外は try/catch で処理することができます:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}

async function consumer() {
  try {
    for await (const value of asyncGenerator()) {
      console.log(value);
    }
  } catch (error) {
    console.error(error.message);
  }
}

consumer();

実行結果は以下の通りです:

33
99
Something went wrong!

非同期ジェネレーター関数の返り値であるイテレータオブジェクトには、同期ジェネレーター関数と同様に throw() メソッドがあります。

イテレータオブジェクトにおいて throw() メソッドを呼び出すと、例外は投げず、代わりにリジェクトされた Promise を投げます:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  yield 11;
}

const go = asyncGenerator();

go.next().then(value => console.log(value));
go.next().then(value => console.log(value));

go.throw(Error("Let's reject!"));

go.next().then(value => console.log(value)); // value is undefined

この状況を処理するには、以下のようにできます:

go.throw(Error("Let's reject!")).catch(reason => console.error(reason.message));

ただし、イテレータオブジェクトの throw()ジェネレーター関数の内部 に例外を送ることを忘れないでおきましょう。これは、以下のようなパターンを適用することを意味します:

async function* asyncGenerator() {
  try {
    yield 33;
    yield 99;
    yield 11;
  } catch (error) {
    console.error(error.message);
  }
}

const go = asyncGenerator();

go.next().then(value => console.log(value));
go.next().then(value => console.log(value));

go.throw(Error("Let's reject!"));

go.next().then(value => console.log(value)); // value is undefined

Node.js のエラー処理

Node.js の同期エラー処理

Node.js における同期エラー処理は、今までみてきた内容とほとんど同じです。

同期コード には、try/catch/finally が使えます。

しかしながら、非同期の世界に目を向けてみると、面白いことが起こります。

Node.js の非同期エラー処理: コールバックパターン

非同期コードにおいては、Node.js は 2 つの書き方に依存しています:

  • コールバックパターン
  • イベントエミッター

コールバックパターン において 非同期 Node.js API は、 イベントループ を通して処理され コールスタック が空になるとすぐに実行されるという関数を引数に取ります。

以下のようなコードを考えてみましょう:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) console.error(error);
    // do stuff with the data
  });
}

上のコードからコールバック関数を抽出すると、どのようにエラーを処理することになっているかを見ることができます:

//
function(error, data) {
    if (error) console.error(error);
    // do stuff with the data
  }
//

fs.readFile の実行過程においてエラーが発生した場合には、エラーオブジェクトを得ます。

この時点で、以下のことが可能です:

  • 今までしてきたように、単純にエラーオブジェクトのログを表示する
  • 例外を投げる
  • 他のコールバックにエラーを渡す

例外を投げる場合は、以下のようにできます:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) throw Error(error.message);
    // do stuff with the data
  });
}

しかし、DOM におけるイベントやタイマーと同様に、この例外は プログラムをクラッシュ させます。以下のように try/catch を用いてクラッシュを阻止しようとしても、うまくいきません:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) throw Error(error.message);
    // do stuff with the data
  });
}

try {
  readDataset("not-here.txt");
} catch (error) {
  console.error(error.message);
}

プログラムをクラッシュさせたくなければ、他のコールバックにエラーを渡すことが望ましい方法です。

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) return errorHandler(error);
    // do stuff with the data
  });
}

ここで用いている eventHandler はその名前からも分かるように、エラーを処理するシンプルな関数です:

function errorHandler(error) {
  console.error(error.message);
  // do something with the error:
  // - write to a log.
  // - send to an external logger.
}

Node.js における非同期エラー処理: イベントエミッター

Node.js で行う多くのことは、 イベント に基づいています。ほとんどの場合、 エミッターオブジェクト と、いくつかのメッセージを待ち受けているオブザーバーとやり取りを行います。

Node.js のイベント駆動なモジュール(例えば net など)はすべて EventEmitter というルートクラスを継承しています。

Node.js の EventEmitter は、2 つの基本的なメソッド持っています: onemit です。

以下のような単純な HTTP サーバーを考えてみましょう:

const net = require("net");

const server = net.createServer().listen(8081, "127.0.0.1");

server.on("listening", function () {
  console.log("Server listening!");
});

server.on("connection", function (socket) {
  console.log("Client connected!");
  socket.end("Hello client!");
});

ここで、以下の 2 つのイベントを待ち受けています: listeningconnection です。

これらのイベントに加えて、イベントエミッターはエラーが発生した際に エラー イベントも発火します。

もしこの上記コードのポート番号を 80 にして実行した場合、以下のような例外を得るでしょう:

const net = require("net");

const server = net.createServer().listen(80, "127.0.0.1");

server.on("listening", function () {
  console.log("Server listening!");
});

server.on("connection", function (socket) {
  console.log("Client connected!");
  socket.end("Hello client!");
});

実行結果:

events.js:291
      throw er; // Unhandled 'error' event
      ^

Error: listen EACCES: permission denied 127.0.0.1:80
Emitted 'error' event on Server instance at: ...

この例外をキャッチするためには、 エラー を待ち受けるイベントハンドラを登録します:

server.on("error", function(error) {
  console.error(error.message);
});

以下のような結果を得ます:

listen EACCES: permission denied 127.0.0.1:80

さらに、プログラムはクラッシュしません。

このトピックについての詳細は、"Error Handling in Node.js" を読むと良いでしょう。

まとめ

このガイドでは、シンプルな同期コードから高度な非同期な仕組みまで、JavaScript のエラー処理全般を扱いました。

JavaScript のプログラムでは、例外の発生の仕方は多岐にわたります。

同期コードの例外は最も単純に対処することができますが、 非同期コード における例外処理は 複雑になる 場合があります。

一方で、ブラウザの新しい JavaScript API はほとんどすべて Promise に向かっています。普及したこのパターンは、then/catch/finally または async/awaittry/catch を使って例外を処理することをより簡単にします。

このガイドを読んだ後は、 プログラムで起こり得るすべての状況を認識して、例外を正しくキャッチすることができる ようになっているはずです。

最後までお読み頂きありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】エラー処理 完全ガイド【保存版】

image.png

本記事は、Valentino Gagliardi 氏の "A mostly complete guide to error handling in JavaScript." を許可を頂いた上で翻訳したものです。

TOC

プログラミングにおけるエラーとは?

私たちの書くプログラムは 常にうまく動作するわけではありません。

時に、プログラムを停止させたり、ユーザーに何か問題が起こったことを知らせたい シチュエーションがあります。

例えば、以下のようなケースがあるでしょう:

  • プログラムが存在しないファイルを開こうとした
  • ネットワークの接続が不調である
  • ユーザーが無効な値を入力した

すべてのケースで、私たちがプログラマーとして、またはプログラミングエンジンを通して、 エラー を作成します。

エラーを作成することで、ユーザーに問題が起きたことをメッセージで伝えたり、プログラムの実行を停止させたりできるのです。

JavaScript におけるエラーとは?

JavaScript におけるエラーはオブジェクト です。このオブジェクトは、後にプログラムを停止するために 投げられる ものです。

JavaScript で新しくエラーを作成するには、適切な コンストラクタ関数 を呼び出します。例えば、一般的なエラーを新規に作成するには以下を実行します:

const err = new Error("Something bad happened!");

new というキーワードを省略することもできます:

const err = Error("Something bad happened!");

一度作成されると、エラーオブジェクトは3つのプロパティを提供します。

  • message: エラーメッセージを含む文字列
  • name: エラーのタイプ
  • stack: 関数実行のスタックトレース

例えば、適当なメッセージ文字列でTypeError オブジェクトを作成した場合、message は実際に渡した文字列となり、name"TypeError"となります:

const wrongType = TypeError("Wrong type given, expected number");

wrongType.message; // "Wrong type given, expected number"
wrongType.name; // "TypeError"

Firefox は上記のプロパティの他に、columnNumberfilenamelineNumberといった非標準プロパティを実装しています。

JavaScript エラー型の種類

JavaScript にはたくさんのエラー型があります。具体的には以下の通りです:

  • Error
  • EvalError
  • InternalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

これらのエラー型は、あたらしいエラーオブジェクトを返す 本物のコンストラクタ関数 であることを忘れないでください。

あなた自身のエラーオブジェクトを作成する際、ErrorTypeError という最も一般的な 2 つのエラー型を使うことが多いでしょう。

エラーの大多数は InternalErrorSyntaxError のように、JavaScript エンジンから直接的に発現するものがほとんどです。

TypeError の一例は、const に再代入しようとした際に発生します:

const name = "Jules";
name = "Caty";

// TypeError: Assignment to constant variable.

SyntaxError の一例は、タイプミスをしたときに発生します:

va x = '33';
// SyntaxError: Unexpected identifier

または、awaitasync 関数以外で利用するなど、予約語を不適切な場所を使った場合にも発生します:

function wrong(){
    await 99;
}

wrong();

// SyntaxError: await is only valid in async function

TypeErrorの他の例としては、ページに存在しない HTML 要素を指定したときに発生します:

Uncaught TypeError: button is null

これらのよくあるエラーオブジェクトに加えて、AggregateError オブジェクトが JavaScript にもうすぐ導入される予定です。後ほど見るように、AggregateError は複数のエラーをまとめる際に便利です。

これらの組み込みエラーに加えて、ブラウザでは以下のようなもの目にすることがあります:

  • DOMException
  • DOMError (Dupulicated, 今は使われていない)

DOMException は Web APIs に関連するエラーファミリーです。ブラウザの中で、ばかげたことをしたときに投げられます。例えば以下のようなことです:

document.body.appendChild(document.cloneNode(true));

結果:

Uncaught DOMException: Node.appendChild: May not add a Document as a child

完全なリストは、MDNのこちらのページを参照してください。

例外とは?

多くのデベロッパーは、エラーと例外を同様のものとして考えています。実際には、 エラーオブジェクトが投げられたときにのみ、エラーオブジェクトが例外になる のです。

JavaScript で例外を投げるには、throwとエラーオブジェクトを用います:

const wrongType = TypeError("Wrong type given, expected number");

throw wrongType;

短縮形のほうがより一般的です。多くのコードベースで以下のようなものを目にするでしょう:

throw TypeError("Wrong type given, expected number");

または

throw new TypeError("Wrong type given, expected number");

関数や条件分岐構文の外で例外を投げることはほとんどありません。代わりに、以下の例を考えてみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

ここでは、関数の引数が文字列(string)かどうかをチェックしています。文字列でなければ、例外を投げます。

JavaScript のルール的には、エラーオブジェクトだけではなく何でも投げることができます:

throw Symbol();
throw 33;
throw "Error!";
throw null;

しかしながら、 プリミティブ型を投げることは避け、適切はエラーオブジェクトを投げるべき です。

そうすることで、コードベースにおいてエラー処理の一貫性を保つことができます。他のチームメンバーがエラーオブジェクトにおいて error.messageerror.stack にアクセスすることができます。

例外を投げると何が起きる?

例外はエレベーターが上に行くようなものです。 一度例外を投げると、どこかで止められない限りプログラムスタックの中でぶくぶくと泡立ってしまします。

以下のようなコードを考えてみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase(4);

このコードをブラウザもしくは Node.js で実行した場合、プログラムは停止し以下のようなエラーを表示します:

Uncaught TypeError: Wrong type given, expected a string
    toUppercase http://localhost:5000/index.js:3
    <anonymous> http://localhost:5000/index.js:9

さらに、エラーが発生した正確な行数を把握することができます。

この表示が スタックトレース であり、プログラムの問題を追跡する際に便利です。

スタックトレースは下から上に積み上がります。つまりここでは以下のようになっていました:

    toUppercase http://localhost:5000/index.js:3
    <anonymous> http://localhost:5000/index.js:9

ここから以下のことが言えます:

  • 9 行目にあるプログラムの何かが toUppercase を呼び出した
  • 3 行目において toUppercase で問題が発生した

ブラウザのコンソールで確認する以外にも、エラーオブジェクトの stack プロパティにアクセスすることによってスタックトレースを見ることができます。

もし例外が キャッチされなかった 場合、つまり、プログラマが例外をキャッチするために何もしなかった場合、プログラムはクラッシュします。

コードの中で、いつ、どこで例外をキャッチするかは、その時々で異なります。

例えば、 プログラムを完全にクラッシュさせるために、例外をスタックに加えて伝播させたいかもしれません。 これは、無効なデータで処理を進めるよりもプログラムを停止させたほうが安全である、といった、致命的なエラーを処理する際に起こりうることです。

さて、ここまでで基本の紹介をしたので、 JavaScript の同期処理と非同期処理における、エラーと例外処理 に話を進めましょう。

同期的エラー処理

同期処理のコードはほとんどの場合単純でわかりやすいので、エラー処理も簡単です。

通常関数のエラー処理

同期処理のコードは、書かれた通りに順番に実行されます。前述のコードをもう一度見てみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase(4);

ここで、JavaScript エンジンは toUppercase を呼び出して実行しています。すべての処理は 同期的 に行われます。このように同期関数から発生する例外を キャッチ するには、try/catch/finally を使うことができます:

try {
  toUppercase(4);
} catch (error) {
  console.error(error.message);
  // or log remotely
} finally {
  // clean up
}

通常、try はハッピーパスや、潜在的に例外を投げる可能性のある関数呼び出しに対して利用します。

catch は、 実際に例外を捉えます。 エラーオブジェクトを受け取り 、エラーの内容を検査することができます(そして本番環境ではログをリモートサーバーに送信したりします)。

一方で、finally ステートメントは、関数の実行結果に関わらず実行されます。つまり、関数が失敗したか成功したかにかかわらず finally 内に書かれたコードは実行されます。

try/catch/finally同期的な 構造であることを覚えておいて下さい。そしていま、 非同期処理のコードから発生する例外をキャッチする方法を獲得した のです。

ジェネレーター関数のエラー処理

JavaScript におけるジェネレーター関数は、関数の特殊な形式です。

この形式の関数は、関数の内側のスコープとその外側の間で 双方向のコミュニケーションチャネル を提供する以外に、 任意に停止したり再開したり することができます。

ジェネレーター関数を作成するには、function キーワードの後ろにアスタリスク * を付けます:

function* generate() {
//
}

そうすると、値を返すために関数内で yield を使用することができます:

function* generate() {
  yield 33;
  yield 99;
}

ジェネレーター関数の返り値イテレータオブジェクト です。ジェネレーターから値を取り出すためには、2つの方法があります:

  • イテレータオブジェクトの next() を呼び出す
  • for...ofイテレーション する

先程の例で、ジェネレーターから値を取り出す場合は、以下のようにできます:

function* generate() {
  yield 33;
  yield 99;
}

const go = generate();

ここで go がイテレータオブジェクトになります。

ここから、go.next() を呼び出し、実行を進めることができます:

function* generate() {
  yield 33;
  yield 99;
}

const go = generate();

const firstStep = go.next().value; // 33
const secondStep = go.next().value; // 99

ジェネレーターは、 呼び出し元から値や例外を受け取ることもできます。

next() に加えて、ジェネレーターから返されたイテレータオブジェクトは、throw() メソッドを持っています。

このメソッドを利用して、ジェネレーターに例外を注入することによってプログラムを停止させてみましょう:

function* generate() {
  yield 33;
  yield 99;
}

const go = generate();

const firstStep = go.next().value; // 33

go.throw(Error("Tired of iterating!"));

const secondStep = go.next().value; // never reached

注入された例外をキャッチするには、ジェネレーター関数内の処理を try/catch 構文で囲む必要があります(必要であれば finally も利用できます):

function* generate() {
  try {
    yield 33;
    yield 99;
  } catch (error) {
    console.error(error.message);
  }
}

ジェネレーター関数は例外を関数の外に投げることもできます。この仕組みは、try/catch/finally を使って同期処理の例外をキャッチするものと同じです。

ジェネレーター関数に対して for...of 構文を利用する例は以下のとおりです:

function* generate() {
  yield 33;
  yield 99;
  throw Error("Tired of iterating!");
}

try {
  for (const value of generate()) {
    console.log(value);
  }
} catch (error) {
  console.error(error.message);
}

/* Output:
33
99
Tired of iterating!
*/

ここでは、try ブロックの中でハッピーパスを実行し、例外があれば catch でキャッチします。

非同期エラー処理

JavaScript はシングルスレッドで実行されるプログラム言語であり、原理的には同期的です。

ブラウザエンジンのようなホスト環境が JavaScript の機能を拡張させたことで、外部のシステムと通信したり、I/O 処理を行うための、たくさんの Web API が使えるようになりました。

ブラウザにおける非同期性の例は タイムアウト(timeouts)、イベント(events)、プロミス(Promise) があります。

非同期の世界におけるエラー処理 は同期の世界におけるそれとは異なります。

いくつか例を見ていきましょう。

タイマーのエラー処理

JavaScript を学び始めたばかりのとき、try/catch/finally 構文について学ぶと、あらゆるコードブロックを try/catch/finally 構文 で囲みたくなるかもしれません。

例えば以下のような関数を考えてみましょう:

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000)
}

この関数は、約 1 秒後にエラーを投げます。この例外を正しく扱うにはどうしたらよいでしょうか?

以下のようなコードは 上手く動きません :

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000);
}

try {
  failAfterOneSecond();
} catch (error) {
  console.error(error.message);
}

前述したように、try/catch 構文は同期的です。一方で、ここでは setTiemout という、タイマー機能を持つブラウザの API を利用しています。

setTimeout に渡したコールバック関数が実行されるときには、既にtry/catch 構文の実行は 終わっている のです。上のプログラムは例外をキャッチすることができず、クラッシュしてしまいます。

2 つの異なったトラックが実行されているのです:

Track A: --> try/catch
Track B: --> setTimeout --> callback --> throw

プログラムをクラッシュさせたくなければ、try/catch 構文を、setTimeout に渡しているコールバック関数の中に移動する必要があります。

しかし、このアプローチは多くの場合意味を成しません。後で見るように、 Promises を用いた非同期エラー処理がより優れている のです。

イベントのエラー処理

Document Object Model (DOM) の HTML ノードは、EventTarget と連携しています。EventTarget は、ブラウザにおけるあらゆるイベントエミッターの共通の祖先といえる存在です。

これはつまり、ページ上の全ての HTML 要素におけるイベントを取得することができることを意味します。

(Node.js も今後のリリースで EventTarget をサポートする予定です)

DOM イベントに対するエラー処理の仕組みは、非同期 Web API における仕組みと同様 です。

以下の例を考えてみましょう:

const button = document.querySelector("button");

button.addEventListener("click", function() {
  throw Error("Can't touch this button!");
});

ここでは、ボタンがクリックされた瞬間に例外を投げています。どのようにその例外をキャッチするのでしょうか?以下のパターンは 上手く動作せず 、プログラムはクラッシュしてしまいます:

const button = document.querySelector("button");

try {
  button.addEventListener("click", function() {
    throw Error("Can't touch this button!");
  });
} catch (error) {
  console.error(error.message);
}

setTimeout の例で見たように、addEventListener に渡されるあらゆるコールバック関数は、非同期的に実行されます:

Track A: --> try/catch
Track B: --> addEventListener --> callback --> throw

プログラムをクラッシュさせたくなければ、addEventListener のコールバック関数内部に try/catch 構文を移動する必要があります。

しかしここでも、そのようにする意味がほぼありません。

setTimeout の例で見たように、非同期処理コードの実行パスにおいて投げられた例外は 外側でキャッチすることができるものではなく 、結果としてプログラムはクラッシュします。

次のセクションで、Promises と async/await がどのように非同期処理におけるエラー処理を手軽なものにするか見ていきます。

onerror はどうだろう?

HTML 要素には、onlickonmouseenteronchange など多くのイベントハンドラがあります。

そのなかには、onerror もありますが、throw やその類のものとは何も関係がありません。

onerror イベントハンドラは、<img><script> のような HTML 要素が存在しないリソースを扱ったときにトリガーされます。

以下のような例を考えてみましょう:

// omitted
<body>
<img src="nowhere-to-be-found.png" alt="So empty!">
</body>
// omitted

上記のような、存在しないリソースを参照する要素を含んだ HTML ドキュメントをブラウザで見ると、コンソールに以下のようなエラーが表示されます:

GET http://localhost:5000/nowhere-to-be-found.png
[HTTP/1.1 404 Not Found 3ms]

JavaScript では、このエラーを以下のように「キャッチ」できるかもしれません:

const image = document.querySelector("img");

image.onerror = function(event) {
  console.log(event);
};

より優れた形で書くと、以下のようになります:

const image = document.querySelector("img");

image.addEventListener("error", function(event) {
  console.log(event);
});

このパターンは、画像やスクリプトなどのリソースに欠損があった際に、代替となるリソースをローディングしたい場合 に便利です。

だたし、onerrorthrowtry/catch とは何の関係もないことを覚えておいて下さい。

Promise を用いたエラー処理

Promise によるエラー処理を説明するために、何度も登場している以下の例を「約束化(promisify)」させてみましょう。以下のコード例を編集していきます:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase(4);

単純に文字列もしくは例外を返す代わりに、成功とエラーを処理するための Promise.rejectPromise.resolve を利用してみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    return Promise.reject(TypeError("Wrong type given, expected a string"));
  }

  const result = string.toUpperCase();

  return Promise.resolve(result);
}

(厳密には、上記コードに非同期処理を行う部分はありませんが、説明するには十分です)

いま、toUppercase 関数は「約束」され、処理結果を扱うために then を、 リジェクトされた Promise を処理するためcatch を使うことができます:

toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message));

上記のコードは、以下のようなログを吐き出します:

Wrong type given, expected a string

Promise において、catch はエラーを処理するための構成要素です。

catchthen に加え、finally もあります。この finally は、try/catch 構文における finally と似たものです。

Promise における finally も、返された Promise の結果に 関わらず 実行されます:

toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message))
  .finally(() => console.log("Run baby, run"));

then/catch/finally に渡されたコールバック関数は、Microtask キューによって非同期に処理されることを覚えておいて下さい。これらは、イベントやタイマーよりも優先される micro task です。

プロミス(Promise)、エラー(error)そしてスロー(throw)

Promise をリジェクトする際は、引数としてエラーオブジェクト渡すのが ベストプラクティス です:

Promise.reject(TypeError("Wrong type given, expected a string"));

そうすることで、エラー処理の一貫性を保つことができます。他のチームメンバーが常に error.message にアクセスすることができますし、さらに重要なことに、スタックトレースを調査することができます。

Promise.reject に加えて、例外を投げることで Promise チェーンから抜け出すことができます。

以下のコード例を考えてみます:

Promise.resolve("A string").then(value => {
  if (typeof value === "string") {
    throw TypeError("Expected a number!");
  }
});

文字列を返すとともに Promise をリゾルブし、そしてその直後に throw によって例外を投げています。

例外の伝播を食い止めるために、通常通り catch を使うことができます:

Promise.resolve("A string")
  .then(value => {
    if (typeof value === "string") {
      throw TypeError("Expected a number!");
    }
  })
  .catch(reason => console.log(reason.message));

このパターンは、fetch を使う際によく用いられます。レスポンスオブジェクトのエラーチェックを行う例は以下の通りです:

fetch("https://example-dev/api/")
  .then(response => {
    if (!response.ok) {
      throw Error(response.statusText);
    }

    return response.json();
  })
  .then(json => console.log(json));

ここでも、catch によって例外を受け取ることができます。もし例外を受け取ることに失敗した場合、あるいはあえて受け取らないことにした場合、 例外はキャッチされるまでスタックに残り続けます。

これは一概に悪いこととは言えませんが、環境によって、キャッチされていないリジェクトに対する挙動は異なります。

例えば、Node.js は将来的に、処理されていない Promise のリジェクトがあった場合は、プログラムをクラッシュさせる予定です:

DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

必ずリジェクトはキャッチしましょう!

"プロミス化"されたタイマーのエラー処理

タイマーとイベントにおいて、コールバック関数内で投げられた例外をキャッチすることは不可能ではありません。前のセクションで、以下のような例を挙げました:

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000);
}

// DOES NOT WORK
try {
  failAfterOneSecond();
} catch (error) {
  console.error(error.message);
}

Promise によって与えられた解決策は、コードの「プロミス化」です。基本的に、Promise でタイマーを囲みます:

function failAfterOneSecond() {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(Error("Something went wrong!"));
    }, 1000);
  });
}

reject によって Promise のリジェクトをセットし、エラーオブジェクトを渡します。

この時点で、catch をつかって例外を処理することができます:

failAfterOneSecond().catch(reason => console.error(reason.message));

Tips: 成功した Promise の返り値の変数名として value、Promise のリジェクトの変数名として reason を使うことが一般的です。

Node.js は promisify と呼ばれる、古い形で書かれたコールバック API をプロミス化するユーティリティを提供しています。

Promise.all のエラー処理

Promise の static メソッドである Promise.all は Promise の配列を引数にとり、リゾルブした Promise の配列を返します。

const promise1 = Promise.resolve("All good!");
const promise2 = Promise.resolve("All good here too!");

Promise.all([promise1, promise2]).then((results) => console.log(results));

// [ 'All good!', 'All good here too!' ]

渡した配列のどれか1つでもリジェクトされた場合、Promise.all は最初にリジェクトされた Promise のエラーとともにリジェクトします。

このような状況を扱うために、前のセクションで見たように catch が利用できます:

const promise1 = Promise.resolve("All good!");
const promise2 = Promise.reject(Error("No good, sorry!"));
const promise3 = Promise.reject(Error("Bad day ..."));

Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error.message));

Promise.all の実行結果に関わらず関数を実行するには、finally を利用します:

Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error.message))
  .finally(() => console.log("Always runs!"));

Promise.any のエラー処理

Promise.any (Firefox > 79, Chrome > 85) は、Promise.all の反対の処理をする関数と考えることができます。

Promise.all が、渡した配列の中に 1 つでもリジェクトされるものがあった場合にエラーを返すのに対し、Promise.any はリジェクトが発生しても、リゾルブしたものが 1 つでもあればそれを返します。

Promise.any に渡した配列に含まれる すべての Promise がリジェクトされた場合、結果として得られるエラーは AggregatedError です。以下のようなコード例を考えてみましょう:

const promise1 = Promise.reject(Error("No good, sorry!"));
const promise2 = Promise.reject(Error("Bad day ..."));

Promise.any([promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log("Always runs!"));

catch を使ってエラーを処理しています。このコードの実行結果は以下の通りです:

AggregateError: No Promise in Promise.any was resolved
Always runs!

AggregatedError オブジェクトは、通常の Error オブジェクトと同様のプロパティに加えて、errors プロパティを持っています:

//
  .catch(error => console.error(error.errors))
//

このプロパティは、それぞれのリジェクトで返されたエラーの配列を格納しています:

[Error: "No good, sorry!, Error: "Bad day ..."]

Promise.race のエラー処理

Promise.race は、Promsie の配列を引数に取ります:

const promise1 = Promise.resolve("The first!");
const promise2 = Promise.resolve("The second!");

Promise.race([promise1, promise2]).then(result => console.log(result));

// The first!

得られる返り値は、 「レース」を制した 1 番着の Promise です。

リジェクトされた場合はどうなるのでしょうか?リジェクトされる Promise が一番でなければ、Promise.race はリゾルブします:

const promise1 = Promise.resolve("The first!");
const rejection = Promise.reject(Error("Ouch!"));
const promise2 = Promise.resolve("The second!");

Promise.race([promise1, rejection, promise2]).then(result =>
  console.log(result)
);

// The first!

もし リジェクトが一番になった場合、Promise.race はリジェクトされ 、以下のようにしてリジェクトをキャッチすることができます:

const promise1 = Promise.resolve("The first!");
const rejection = Promise.reject(Error("Ouch!"));
const promise2 = Promise.resolve("The second!");

Promise.race([rejection, promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error.message));

// Ouch!

Promise.allSettled のエラー処理

Promise.allSettled は ECMAScript 2020 で追加される関数です。

この関数を使って処理するケースはそれほど多くありません。なぜなら、 Promise のリジェクトがあったとしても、返り値は常にリゾルブされた Promise になるため です。

以下のようなコード例を考えてみます:

const promise1 = Promise.resolve("Good!");
const promise2 = Promise.reject(Error("No good, sorry!"));

Promise.allSettled([promise1, promise2])
  .then(results => console.log(results))
  .catch(error => console.error(error))
  .finally(() => console.log("Always runs!"));

上の例では、リゾルブする Promise とリジェクトされる Promise を 1 つずつ含め、配列として渡しています。

このケースでは、catch が実行されることはありません。代わりに finally が実行されます。

then 内の処理によってロギングされる結果は次の通りです:

[
  { status: 'fulfilled', value: 'Good!' },
  {
    status: 'rejected',
    reason: Error: No good, sorry!
  }
]

async/await のエラー処理

JavaScript の async/await は非同期関数を表しますが、コードを読む立場からみれば、同期関数の 可読性の高さ の恩恵を受けているといえます。

話を単純にするために、何度も登場している同期関数 toUppercase を、function キーワードの前に async を付け足すことで、非同期関数に変換します:

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

async というプレフィックスを使うことで、関数に Promise を返す ように仕向けることができるようになります。これはつまり、thencatchfinally といったチェーンが使えることを意味します:

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase("abc")
  .then(result => console.log(result))
  .catch(error => console.error(error.message))
  .finally(() => console.log("Always runs!"));

async 関数内で例外を投げた 場合、この例外は、 裏側で機能している Promise をリジェクト させます。

どんなエラーも、catch によってキャッチすることができます。

最も重要なことは、このスタイルに加えて同期関数と同様に try/catch/finally 構文を使える、ということです。

以下の例では、toUppercase 関数を consumer という他の関数から呼び出しています。consumer 内部では、toUppercase 関数を try/catch/finally 関数で囲っています:

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

async function consumer() {
  try {
    await toUppercase(98);
  } catch (error) {
    console.error(error.message);
  } finally {
    console.log("Always runs!");
  }
}

consumer(); // Returning Promise ignored

実行結果は以下の通りです:

Wrong type given, expected a string
Always runs!

同様のトピックは次の記事でも扱っています: How to Throw Errors From Async Functions in JavaScript?

非同期ジェネレーターのエラー処理

JavaScript の 非同期ジェネレーター は、通常の値の代わりに Promise を yeild することができるジェネレーター関数 です。

async とジェネレーター関数を組み合わせて使います。イテレータオブジェクトが呼び出し元に対して Promise を返すジェネレーター関数です。

非同期ジェネレーターを作るために、async でプレフィックスした、* を持つ関数を定義します:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}

Promise の仕組みに基づいているため、エラー処理に対しても同様のルールが適用されます。非同期ジェネレーター関数内の throw は Promise のリジェクトに繋がり、catch でキャッチすることができます。

非同期ジェネレーター関数から Promise を取り出す には、以下の 2 つのアプローチがあります。

  • thenハンドラ
  • 非同期イテレーション

上のコード例では、最初の 2 つの値が yield されたあとに、例外が投げられます。これは以下のようにできることを意味します:

const go = asyncGenerator();

go.next().then(value => console.log(value));
go.next().then(value => console.log(value));
go.next().catch(reason => console.error(reason.message));

上記コードの実行結果は以下の通りです:

{ value: 33, done: false }
{ value: 99, done: false }
Something went wrong!

もう1つのアプローチは、 for await...of非同期イテレーション を用いる方法です。非同期イテレーションを用いるためには、呼び出し側の関数を async で囲む必要があります。

以下が完全なコード例です:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}

async function consumer() {
  for await (const value of asyncGenerator()) {
    console.log(value);
  }
}

consumer();

async/await で見たように、潜在的に存在する例外は try/catch で処理することができます:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}

async function consumer() {
  try {
    for await (const value of asyncGenerator()) {
      console.log(value);
    }
  } catch (error) {
    console.error(error.message);
  }
}

consumer();

実行結果は以下の通りです:

33
99
Something went wrong!

非同期ジェネレーター関数の返り値であるイテレータオブジェクトには、同期ジェネレーター関数と同様に throw() メソッドがあります。

イテレータオブジェクトにおいて throw() メソッドを呼び出すと、例外は投げず、代わりにリジェクトされた Promise を投げます:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  yield 11;
}

const go = asyncGenerator();

go.next().then(value => console.log(value));
go.next().then(value => console.log(value));

go.throw(Error("Let's reject!"));

go.next().then(value => console.log(value)); // value is undefined

この状況を処理するには、以下のようにできます:

go.throw(Error("Let's reject!")).catch(reason => console.error(reason.message));

ただし、イテレータオブジェクトの throw()ジェネレーター関数の内部 に例外を送ることを忘れないでおきましょう。これは、以下のようなパターンを適用することを意味します:

async function* asyncGenerator() {
  try {
    yield 33;
    yield 99;
    yield 11;
  } catch (error) {
    console.error(error.message);
  }
}

const go = asyncGenerator();

go.next().then(value => console.log(value));
go.next().then(value => console.log(value));

go.throw(Error("Let's reject!"));

go.next().then(value => console.log(value)); // value is undefined

Node.js のエラー処理

Node.js の同期エラー処理

Node.js における同期エラー処理は、今までみてきた内容とほとんど同じです。

同期コード には、try/catch/finally が使えます。

しかしながら、非同期の世界に目を向けてみると、面白いことが起こります。

Node.js の非同期エラー処理: コールバックパターン

非同期コードにおいては、Node.js は 2 つの書き方に依存しています:

  • コールバックパターン
  • イベントエミッター

コールバックパターン において 非同期 Node.js API は、 イベントループ を通して処理され コールスタック が空になるとすぐに実行されるという関数を引数に取ります。

以下のようなコードを考えてみましょう:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) console.error(error);
    // do stuff with the data
  });
}

上のコードからコールバック関数を抽出すると、どのようにエラーを処理することになっているかを見ることができます:

//
function(error, data) {
    if (error) console.error(error);
    // do stuff with the data
  }
//

fs.readFile の実行過程においてエラーが発生した場合には、エラーオブジェクトを得ます。

この時点で、以下のことが可能です:

  • 今までしてきたように、単純にエラーオブジェクトのログを表示する
  • 例外を投げる
  • 他のコールバックにエラーを渡す

例外を投げる場合は、以下のようにできます:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) throw Error(error.message);
    // do stuff with the data
  });
}

しかし、DOM におけるイベントやタイマーと同様に、この例外は プログラムをクラッシュ させます。以下のように try/catch を用いてクラッシュを阻止しようとしても、うまくいきません:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) throw Error(error.message);
    // do stuff with the data
  });
}

try {
  readDataset("not-here.txt");
} catch (error) {
  console.error(error.message);
}

プログラムをクラッシュさせたくなければ、他のコールバックにエラーを渡すことが望ましい方法です。

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) return errorHandler(error);
    // do stuff with the data
  });
}

ここで用いている eventHandler はその名前からも分かるように、エラーを処理するシンプルな関数です:

function errorHandler(error) {
  console.error(error.message);
  // do something with the error:
  // - write to a log.
  // - send to an external logger.
}

Node.js における非同期エラー処理: イベントエミッター

Node.js で行う多くのことは、 イベント に基づいています。ほとんどの場合、 エミッターオブジェクト と、いくつかのメッセージを待ち受けているオブザーバーとやり取りを行います。

Node.js のイベント駆動なモジュール(例えば net など)はすべて EventEmitter というルートクラスを継承しています。

Node.js の EventEmitter は、2 つの基本的なメソッド持っています: onemit です。

以下のような単純な HTTP サーバーを考えてみましょう:

const net = require("net");

const server = net.createServer().listen(8081, "127.0.0.1");

server.on("listening", function () {
  console.log("Server listening!");
});

server.on("connection", function (socket) {
  console.log("Client connected!");
  socket.end("Hello client!");
});

ここで、以下の 2 つのイベントを待ち受けています: listeningconnection です。

これらのイベントに加えて、イベントエミッターはエラーが発生した際に エラー イベントも発火します。

もしこの上記コードのポート番号を 80 にして実行した場合、以下のような例外を得るでしょう:

const net = require("net");

const server = net.createServer().listen(80, "127.0.0.1");

server.on("listening", function () {
  console.log("Server listening!");
});

server.on("connection", function (socket) {
  console.log("Client connected!");
  socket.end("Hello client!");
});

実行結果:

events.js:291
      throw er; // Unhandled 'error' event
      ^

Error: listen EACCES: permission denied 127.0.0.1:80
Emitted 'error' event on Server instance at: ...

この例外をキャッチするためには、 エラー を待ち受けるイベントハンドラを登録します:

server.on("error", function(error) {
  console.error(error.message);
});

以下のような結果を得ます:

listen EACCES: permission denied 127.0.0.1:80

さらに、プログラムはクラッシュしません。

このトピックについての詳細は、"Error Handling in Node.js" を読むと良いでしょう。

まとめ

このガイドでは、シンプルな同期コードから高度な非同期な仕組みまで、JavaScript のエラー処理全般を扱いました。

JavaScript のプログラムでは、例外の発生の仕方は多岐にわたります。

同期コードの例外は最も単純に対処することができますが、 非同期コード における例外処理は 複雑になる 場合があります。

一方で、ブラウザの新しい JavaScript API はほとんどすべて Promise に向かっています。普及したこのパターンは、then/catch/finally または async/awaittry/catch を使って例外を処理することをより簡単にします。

このガイドを読んだ後は、 プログラムで起こり得るすべての状況を認識して、例外を正しくキャッチすることができる ようになっているはずです。

最後までお読み頂きありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nodeが壊れた】internal/modules/cjs/loader.js:582 throw err;

Reactで作られたサービスをクローンして勉強していたら、Nodeが壊れた(のかもしれない)

internal/modules/cjs/loader.js:582
    throw err;
    ^

Error: Cannot find module 'C:\Users\User\Desktop\NodeJsProject\app.js'

解決策

理由はわからんが、以下のコマンドを打ったらうまくいった

$ npm uninstall --save-dev request
$ npm install --save request
$ npm start

スクリーンショット 2020-09-21 15.44.07.png

参考
https://stackoverflow.com/questions/53545800/internal-modules-cjs-loader-js582-throw-err
https://qiita.com/TakuTaku04/items/02ff2f4555f705e8c055

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Bitcoinを自動売買】AWSのDockerで運用してみた話、外出先のAppleWatchから1TAPでON/OFF&Line通知付き

autocoin2

BitCoinのFX自動売買プログラム。
BitflyerのAPIを利用して、node.jsにて仮想通貨トレードを自動化しました。
寝てる時、トイレ中、24時間中、お金が勝手に増えてくれたら、どんなに素敵だろう。。:gem:
楽して自動的に儲かりたい!そんなダメ人間モチベーションで作ってみました。
iOS の画像.jpgスクリーンショット 2020-09-20 10.05.38.png

いきなり結論ですが、、、残念ながら儲かりません:scream:
むしろ、減っています。。

ですが、チューニングしたら、ひょっとしたら儲かり出すかもしれません。
(損害を受けても当方は一切責任はありません。)
あくまで、自己責任でお願いします!

Githubにコード公開しています

特徴

  • 売り・買いポジション両方対応
  • 複数アルゴリズムによる重み付け売買判断
  • MongoDBによる売買履歴の保存
  • 取引開始をLine通知
  • 損得金額の閾値を超えたら、Lineにて通知
  • 一定の日数が経過したら、ポジションを自動で手放す機能
  • 日付変更30分前には、新たなポジション取得を抑制する機能
  • Apple Home連携で外出先でもiphoneから1タップでON/OFF
  • プログラム稼働中でも、並行して通常の人的トレードも可能

システム概要

autocoin2.png

使用技術

  • Node.js
  • Docker
  • AWS
  • MongoDB
  • shell
  • Raspberry Pi

ディレクトリ構成

.
├── autocoin
│  ├── algo.js
│  ├── app.js
│  ├── config.js
│  ├── crypto.js
│  ├── line.js
│  ├── mongo.js
│  └── utils.js
├── container_data
├── homebridge_AWS
│  ├── startAWS.sh
│  └── stopAWS.sh
├── .env
├── Dockerfile
└── docker-compose.yml

メイン:app.js

このプログラムのエントリーポイント。
ループ処理でコードを廻すことで売買を繰り返します。

'use strict';
const config = require('./config');
const moment = require('moment');
const ccxt = require('ccxt');
const bitflyer = new ccxt.bitflyer(config);

const Crypto = require('./crypto')
const Mongo = require('./mongo');
const mongo = new Mongo();
const Line = require('./line');
const line = new Line(config.line_token)
const utils = require('./utils');
const Algo = require('./algo');

//取引間隔(秒)
const tradeInterval = 180;
//取引量
const orderSize = 0.01;
//swap日数
const swapDays = 3;
//通知用の価格差閾値
const infoThreshold = 100;

//psychoAlgoの設定値;陽線カウント
const psychoParam = {
  'range': 10,
  'ratio': 0.7,
};
//crossAlgoの設定値:移動平均幅
const crossParam = {
  'shortMA': 5,
  'longMA': 30,
};

//ボリンジャーバンド設定値
const BBOrder = {
  //注文
  'period': 10,
  'sigma': 1.7
};
const BBProfit = {
  //利確
  'period': 10,
  'sigma': 1
};
const BBLossCut = {
  //損切り:日足で判断
  'period': 10,
  'sigma': 2.5
};

// アルゴリズムの重み付け:未使用は0にする
const algoWeight = {
  // 'psychoAlgo': 0,
  // 'crossAlgo': 0,
  // 'bollingerAlgo': 1,
  'psychoAlgo': 0.1,
  'crossAlgo': 0.2,
  'bollingerAlgo': 0.7,
};
//取引判断の閾値
const algoThreshold = 0.3;
//ロスカットの閾値
const lossCutThreshold = 0.5;


(async function () {
  let sumProfit = 0;
  let beforeProfit = null;
  const nowTime = moment();
  const collateral = await bitflyer.fetch2('getcollateral', 'private', 'GET');

  //(分)レコード作成
  const crypto = new Crypto();
  const beforeHour = crossParam.longMA * tradeInterval;
  const timeStamp = nowTime.unix() - beforeHour;
  let records = await crypto.getOhlc(tradeInterval, timeStamp);

  const algo = new Algo(records);

  //Lineに自動売買スタートを通知
  const strTime = nowTime.format('YYYY/MM/DD HH:mm:ss');
  const message = `\n 自動売買スタート\n date: ${strTime}\n collateral: ${collateral.collateral}`;
  line.notify(message);


  while (true) {
    let flag = null;
    let label = "";
    let tradeLog = null;

    const nowTime = moment();
    const strTime = nowTime.format('YYYY/MM/DD HH:mm:ss');

    //取引所の稼働状況を確認
    let health = await bitflyer.fetch2('getboardstate');
    if (health.state !== 'RUNNING') {
      // 異常ならwhileの先頭に
      console.log('取引所の稼働状況:', health);
      await utils.sleep(tradeInterval * 1000);
      continue;
    }

    //現在価格を取得
    const ticker = await bitflyer.fetchTicker('FX_BTC_JPY');
    const nowPrice = ticker.close;

    //レコードを更新
    algo.records.push(nowPrice);
    algo.records.shift()

    //アルゴリズム用Paramを初期化
    let bbRes = null;
    let totalEva = 0;
    algo.initEva();
    //共通アルゴリズム
    let crossRes = algo.crossAlgo(crossParam.shortMA, crossParam.longMA);
    let psychoRes = algo.psychoAlgo(psychoParam.range, psychoParam.ratio)

    //建玉を調べる
    const jsonOpenI = await bitflyer.fetch2('getpositions', 'private', 'GET', {product_code: "FX_BTC_JPY"});
    const openI = utils.chkOpenI(jsonOpenI)

    //共通表示
    console.log('================');
    console.log('time:', strTime);
    console.log('nowPrice: ', nowPrice);


    // 建玉がある場合
    if (openI.side) {
      //建玉の共通表示
      console.log('');
      console.log('建玉内容');
      console.log(openI);

      let diffDays = nowTime.diff(openI.open_date, 'days');
      // swap日数を超えているなら
      if (diffDays >= swapDays) {
        // 建玉を0に戻す
        label = 'swap日数を超えているため建玉をリセット'

        if (openI.side === 'BUY') {
          await bitflyer.createMarketSellOrder('FX_BTC_JPY', openI.size);
          flag = 'SELL';

        } else {
          await bitflyer.createMarketBuyOrder('FX_BTC_JPY', openI.size);
          flag = 'BUY';
        }
        sumProfit += openI.pnl;

      } else {
        // 日数を超えてないなら
        //  利益が出ている場合
        if (openI.pnl > 0) {
          label = '利確'
          bbRes = algo.bollingerAlgo(BBProfit.period, BBProfit.sigma, openI.price);
          totalEva = algo.tradeAlgo(algoWeight)

          //買い建玉で、下降シグナルが出ている
          if (openI.side === 'BUY' && totalEva < -algoThreshold) {
            await bitflyer.createMarketSellOrder('FX_BTC_JPY', openI.size);
            sumProfit += openI.pnl;
            flag = 'SELL';

            //売り建玉で、上昇シグナルが出ている
          } else if (openI.side === 'SELL' && totalEva > algoThreshold) {
            await bitflyer.createMarketBuyOrder('FX_BTC_JPY', openI.size);
            sumProfit += openI.pnl;
            flag = 'BUY';

          }
        } else {
          //  損してる場合
          label = 'ロスカット';

          //日足でアルゴリズム判断
          const dayPeriods = 60 * 60 * 24;
          const lossTimeStamp = nowTime.unix() - dayPeriods * BBLossCut.period;
          let dayRecords = await crypto.getOhlc(dayPeriods, lossTimeStamp);

          crossRes = algo.crossAlgo(crossParam.shortMA, crossParam.longMA, dayRecords);
          psychoRes = algo.psychoAlgo(psychoParam.range, psychoParam.ratio, dayRecords);
          bbRes = algo.bollingerAlgo(BBLossCut.period, BBLossCut.sigma, openI.price, dayRecords);
          totalEva = algo.tradeAlgo(algoWeight)

          //損してるのに、買いを持ってて大きなトレンドが下がり兆候
          if (openI.side === 'BUY' && totalEva < -lossCutThreshold) {
            await bitflyer.createMarketSellOrder('FX_BTC_JPY', openI.size);
            sumProfit += openI.pnl;
            flag = 'SELL';

            //損してるのに、売りを持ってて大きなトレンドで上がり兆候
          } else if (openI.side === 'SELL' && totalEva > lossCutThreshold) {
            await bitflyer.createMarketBuyOrder('FX_BTC_JPY', openI.size);
            sumProfit += openI.pnl;
            flag = 'BUY';
          }
        }
      }

      //建玉を精算したなら、
      if (flag) {
        tradeLog = {
          flag: flag,
          label: label,
          sumProfit: sumProfit,
          profit: openI.pnl,
          nowPrice: nowPrice,
          openPrice: openI.price,
          strTime: strTime,
          created_at: nowTime._d,
          openI: openI,
          bollinger: bbRes,
          cross: crossRes,
          psycho: psychoRes,
          totalEva: totalEva,
        };
        mongo.insert(tradeLog);

        console.log('');
        console.log(label);
        console.log(tradeLog);
      }

      // Line通知(閾値を超えたら)
      if (beforeProfit !== null) {
        const profit = openI.pnl;
        const diff = Math.abs(sumProfit + profit - beforeProfit);
        if (diff >= infoThreshold) {
          const message = `\n date: ${strTime}\n sumProfit: ${sumProfit}\n profit: ${profit}\n collateral: ${collateral.collateral}`;
          line.notify(message);
          beforeProfit = sumProfit + profit;
        }
      } else {
        //アラート初期化
        beforeProfit = sumProfit;
      }


    } else {
      //建玉を持ってない場合

      //スワップポイント対応 23:30-0:00 注文しない
      const limitDay = moment().hours(23).minutes(30).seconds(0)
      if (nowTime.isSameOrAfter(limitDay)) {
        console.log(' ');
        console.log('スワップポイント対応中_23:30-0:00');
        //注文を受け付けない while先頭に移動
        await utils.sleep(tradeInterval * 1000);
        continue;
      }

      // 注文する ボリンジャーを使用
      bbRes = algo.bollingerAlgo(BBOrder.period, BBOrder.sigma);
      totalEva = algo.tradeAlgo(algoWeight)

      if (totalEva > algoThreshold) {
        //【買い】で建玉する
        await bitflyer.createMarketBuyOrder('FX_BTC_JPY', orderSize);
        flag = 'BUY';

      } else if (totalEva < -algoThreshold) {
        //【売り】で建玉する
        await bitflyer.createMarketSellOrder('FX_BTC_JPY', orderSize);
        flag = 'SELL';
      }

      //建玉を取得したなら、
      if (flag) {
        label = '建玉取得';

        tradeLog = {
          flag: flag,
          label: label,
          sumProfit: sumProfit,
          nowPrice: nowPrice,
          bollinger: bbRes,
          cross: crossRes,
          psycho: psychoRes,
          totalEva: totalEva,
          strTime: strTime,
          created_at: nowTime._d,
        };
        mongo.insert(tradeLog);

        console.log('');
        console.log(label);
        console.log(tradeLog);
      }
    }

    console.log('');
    console.log('★sumProfit: ', sumProfit);
    console.log('');
    await utils.sleep(tradeInterval * 1000);
  }
})();


ハイパーパラメーターの説明

  • tradeInterval: 取引間隔。最短は60秒。
  • orderSize: 注文数
  • swapDays: 建玉の保持したい日数。超過したら手放す。
  • infoThreshold: Line告知用の金額幅。設定額を超えた損得をするとLine告知する。
  • psychoParam: サイコロジカルラインのアルゴリズムに使用するパラメーター。
    • 期間
    • 比率
  • crossParam: ゴールデンクロス・デッドクロスのアルゴリズムに使用するパラメーター。
    • 短期移動平均線の期間
    • 長期移動平均線の期間
  • BBOrder / BBProfit / BBLossCut: ボリンジャーバンドのアルゴリズムに使用するパラメーター。 建玉取得 / 利益確定 / 損切りごとに判断材料が異なるため分けています。
    • 判断期間
    • 標準偏差
  • algoWeight: 各アルゴリズムの重み(重要度)を設定。 重要度はただの比率なので合計1になるような調整をおすすめ。
  • algoThreshold: 取引判断の閾値。 アルゴリズムの複合判断された値がこの値以上(以下)であれば取引。
  • lossCutThreshold: ロスカット判断の閾値。 複合判断された値がこの値以上(以下)であればロスカット。

分岐、流れの紹介説明

大まかな処理の流れです。

  • 売買スタート
    判断材料とするため、cryptowatchからコード実行前の取引内容を取得する。
    Lineで「自動売買スタート」したことを通知

  • 取引ループを開始
    設定した取引間隔でループを廻す。

  • bitflyerの取引所の稼働状況を確認
    異常であれば、ループの先頭に移動

  • 現在のbitcoinの価格を取得

  • 共通利用アルゴリズム
    クロス関数、サイコロジカル関数の評価をする

  • 保持している建玉(ポジション)の内容を取得する。

  • 建玉を保持している場合、建玉の保持日数を調べる
    保持日数が、指定日数より長ければ、建玉を手放す
    (swap金と、塩漬けされるのを回避するため。)

  • 保持日数が短い場合

  • 利益が出ていれば、ポジションと、アルゴリズム判断により売買

  • 損が出ていれば、日足材料でのアルゴリズム判断によりロスカット
    ロスカットが日足利用なのは、分足だと指標が流動的過ぎ、大きなトレンドで判断が必要と考慮したため。
    実際、昔は分足を使っていたのですが、かなり細かいブレに振り回され利得チャンスを失った上、小さな損を積み上げやすかったです。
    分足、時間足に変更は可能なので調整してみるのもいいかもしれません。

  • 一定額の損得が発生したら、Lineで通知。

  • 建玉を持っていない場合、

  • 日付変更直前の30分なら、取引せずにループ。
    建玉取得して早々にswap金が発生するのを避けるため。

  • 日付直前じゃなければ、アルゴリズム判断により建玉を取得する。

アルゴリズム:algo.js

売買アルゴリズムをまとめています。

const gauss = require('gauss');

module.exports = class Algo {

  constructor(records) {
    this.records = records;

    // 各アルゴリズムの評価ポイント
    //上昇シグナル:+  下降シグナル:-
    this.eva = {
      'psychoAlgo': 0,
      'crossAlgo': 0,
      'bollingerAlgo': 0
    };
  }

  psychoAlgo(range, ratio, list = this.records) {
    //  陽線の割合で売買を判断する

    let countHigh = 0
    //  任意期間の陽線回数をカウント
    for (let i = range; i > 0; i--) {
      const before = list[list.length - i - 1];
      const after = list[list.length - i];

      if (before <= after) {
        countHigh += 1;
      }
    }

    let psychoRatio = 0;
    psychoRatio = countHigh / range;
    if (psychoRatio >= ratio) {
      this.eva['psychoAlgo'] = 1;
    } else if (psychoRatio <= 1 - ratio) {
      this.eva['psychoAlgo'] = -1;
    }

    return psychoRatio;
  }


  crossAlgo(shortMA, longMA, list = this.records) {
    //ゴールデン・デッドクロスで売買を判断する

    //移動平均作成
    const prices = new gauss.Vector(list);
    const shortValue = prices.ema(shortMA).pop();
    const longValue = prices.ema(longMA).pop();

    if (shortValue >= longValue) {
      this.eva['crossAlgo'] = 1;
    } else if (shortValue < longValue) {
      this.eva['crossAlgo'] = -1;
    }

    return {'shortValue': shortValue, 'longValue': longValue};
  }


  bollingerAlgo(period, sigma, price = this.records.slice(-1)[0], list = this.records) {
    //  ボリンジャーバンド

    const prices = new gauss.Vector(list.slice(-period));
    //SMA使用
    const sma = prices.sma(period).pop();
    const stdev = prices.stdev()

    const upper = Math.round(sma + stdev * sigma);
    const lower = Math.round(sma - stdev * sigma);

    if (price <= lower) {
      this.eva['bollingerAlgo'] = 1;
    } else if (price >= upper) {
      this.eva['bollingerAlgo'] = -1;
    }

    return {'upper': upper, 'lower': lower}
  }


  tradeAlgo(weight) {
    //  重み付けして総合的な取引判断

    let totalEva = 0
    //評価ポイントにそれぞれの重みを掛けて足し合わせる
    for (const [key, value] of Object.entries(this.eva)) {
      totalEva += value * weight[key];
    }

    totalEva = Math.round(totalEva * 100) / 100

    return totalEva
  }


  initEva() {
    //全評価ポイントを初期化
    Object.keys(this.eva).forEach(key => {
      this.eva[key] = 0;
    });
  }

}

複合評価

tradeAlgo()

取引判断は複数のアルゴリズムによる複合判断です。
アルゴリズムごとにそれぞれ評価ポイントを保持。
各アルゴリズムは材料データと設定パラメータにより、-1か、+1どちらかの評価をつける。
正数(+1)は上昇トレンド
負数(-1)は下降トレンド
各アルゴリズムごとに評価ポイントとその重みで掛け算し、最後に全て足し合わせて総合評価ポイントを算出します。

app.js内で閾値より、総合評価ポイントの絶対値が上回っていれば取引を実行。
買/売どちらのポジションで取引するかは、状況に応じてapp.jsで判断。

アルゴリズムの追加について

今後、新たなアルゴリズムを追加したい場合は以下の手順を参考。

  • Algoクラス

    • this.eva(メソッド名と同じ評価ポイントの追加)
    • methodとしてアルゴリズムの追加
  • app.js

    • 重み付けの追加
    • 評価させたい箇所でメソッドを追加
      (恐らく共通アルゴリズムとしてcrossAlgo()などと同じ箇所が多いと思います。)

ボリンジャーバンド

bollingerAlgo()

ボリンジャーバンドは、移動平均線と標準偏差を使った判断アルゴリズム。
ざっくり、標準偏差の絶対値が大きければ大きいほど平均に回帰する力が強くなるっていう考え方。
細かく触れないですが、こちらの説明が分かりやすいです。
マネックス証券解説

4つの変数を使用。

  • 判断期間
  • 閾値にする標準偏差
  • 判断したい値段
  • 値動きリスト

値動きリストから判断したい期間を取り出す。
次に、取り出したリストをもとに指定した標準偏差のupper値とlower値を算出する。
最後に、算出した上下の標準偏差帯より、価格がはみ出していれば評価ポイントをつける。

lower値より価格が低い場合
その価格はトレンドより低めに付けられているので、上昇に転じやすい。
上昇トレンドとして、+1をつける

upper値より価格が高い場合
その価格はトレンドより高めに付けられているので、下降に転じやすい。
下降トレンドとして、 -1をつける

サイコロジカルライン

psychoAlgo()

投資家心理を利用したアルゴリズム判断。
売り買いのどちらかが連続して偏った場合、更にその傾向は続く、またはそろそろ逆が出ると判断して多くの売買が行われることで価格変動を予想するアルゴリズム。
このページが分かりやすいです。
マネックス証券解説

3つの変数を使用。

  • 判断範囲
  • 判断する比率
  • 値段リスト

設定期間の値段リストに絞り込み、
一つ前の値段より上昇している値段の数と全体の値段の割合を調べる。
割合値と判断する比率を利用比較して、上昇トレンド、下降トレンドと評価ポイントを付ける。

ゴールデンクロス、デッドクロス

crossAlgo()

長期移動平均線が、短期移動平均線を下から上に突き抜けることをゴールデンクロス。その逆がデッドクロス。
ゴールデンは上昇トレンドになるサインで、デッドクロスは下降トレンドのサイン。

3つの変数を使用。

  • 短期期間
  • 長期間
  • 値段リスト

上記説明の通り、短期移動平均が長期移動平均より上なら上昇トレンドとして評価ポイント+1をつける。
その逆ならデッドクロスとして評価ポイントに-1をつける。
なお、よりトレンドの勢いを加味したかったので、直近値動きをより重視する指数平滑移動平均線(Exponential Moving Average)を使用しました。

OHLCの取得:crypto.js

プログラム実行直後のOHLC取得にcryptowatchAPIを使用。
OHLCはopen/high/low/closeとローソク足データのことです。

Line通知:line.js

取引開始と、一定額以上の損得が発生する毎にLineから通知をします。

Line Nnotifyに関してはこちらのページが分かりやすいです。
LINE notify を使ってみる

取引内容の保存:mongo.js

MongoDBで、売買取引を記録。
取引内容はbitflyerAPIからのjson取得で定形データでは無いので、NoSQLのMongoDBを選択しました。

Dockerコンテナによる稼働ですが、volumesでデータを永続化しています。
最初のDockerコンテナ立ち上げで、volumeディレクトリ:container_dataが作成されます。

その他:utils.js

その他のユーティリティ関数をまとめています。

Apple Home連動

iphone,AppleWatchから1タップで、AWS上のプログラムのON/OFFできるようにしました。

  1. AWS EC2インスタンスにDockerを展開
  2. 自宅のRaspberry Piにhomebridge展開し、実行のshellファイルを紐付ける

詳しくは以下を参考ください。
公式homebridge

Philips_HueをAPI連動!〜ラズパイをHomeKit化する

IntelliJで取引DBを見る

MongoDBに保存した取引内容ですが、閲覧はIDEの利用をおすすめします。
直接MongoDBからの閲覧は、json形式のためかなり辛いです。
IntelliJなどのIDEでしたら、いい感じに見やすくしてくれます。
スクリーンショット 2020-09-20 10.18.45.png
スクリーンショット 2020-09-20 10.11.57.png
IntelliJの設定方法は、過去記事を参照ください。
IntelliJからAWSを操作する設定方法まとめ

注意点

最初のDocker立ち上げ

MongoDBが書き込み可能になるのに時間かかります。
volumeディレクトリのcontainer_dataを作成するためです。
余裕持って初回起動して、しばらくしても書き込みされなければ、再度Dockerを立ち上げ直してください。
私の場合ですが、2回目以降は問題なく稼働しています。

Docker Volumeのcontainer_dataはroot権限

作成されるvolumeディレクトリのcontainer_dataはroot権限で作られます。削除したい際はsudoを付けてください。
私のうっかり経験ですが、Dockerを再ビルドする際に、このディレクトリ権限に気付かずエラーになって少しハマりました。

終わりに

楽して不労所得の夢は実現しません:skull::skull:
しつこいですが、必ず儲かるわけではないですし、自己責任でお願いします。:point_up_tone3:

ひょっとしたら、
私のアルゴリズムや、パラメーターがイマイチナだけで、誰かが追加したアルゴリズムにより儲かりだすかもしれません。。。

そんな時は、こっそりと教えて下さいね:musical_note:
それでは、よろしければホビーとしてほどほどに楽しんでみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npm から取得した Node-RED をカスタマイズして起動する(2)

何のため?

Node-RED を Heroku で動作させたかったのだが想定外の動作をしていました。
Node-RED で発生している事象を調査するための環境構築手順を整理した記録です。

セオリーから外れているかもしれませんので、間違っていたらコメントをお願いします。

0. 準備

前の記事で node-red-sample ディレクトリ配下に Node-RED のソースがある状態の環境を構築した。
本記事では、『2.2 カスタマイズ方法(2) : settings.js を userDir に入れておく』 の方法で進めていく。

準備としては、下記コマンドを入力したところから開始となります。

$ mkdir node-red-sample
$ cd node-red-sample
$ npm -y init
$ npm install node-red
$ mkdir data
$ cp ./node_modules/node-red/settings.js ./data/

Node-RED を起動するには下記コマンドを入力する。

$ node ./node_modules/node-red/red.js  --userDir ./data

1. npm で入手した Node-RED をローカル管理に変更

前述の通り、Node-RED のコアをデバッグしたいので、ソース管理内に保持する手順を実施する。

1.1. Node-RED をローカルフォルダに移動

改変を加える Node-RED ソースは local_packages ディレクトリに置いて作業を進める事にする。

$ mkdir local_packages
$ mv node_modules/node-red ./local_packages/
$ mv node_modules/@node-red ./local_packages/
# 念のため依存パッケージからnpmjsのnode-redを取り除く
$ npm uninstall node-red
$ rm package-lock.json

1.2. package.json を書き換え

local_packages フォルダのモジュール類を dependencies 記述する。

package.json
  "dependencies": {
    "node-red": "file:local_packages/node-red",
    "@node-red/nodes": "file:local_packages/@node-red/nodes",
    "@node-red/editor-api": "file:local_packages/@node-red/editor-api",
    "@node-red/editor-client": "file:local_packages/@node-red/editor-client",
    "@node-red/registry": "file:local_packages/@node-red/registry",
    "@node-red/runtime": "file:local_packages/@node-red/runtime",
    "@node-red/util": "file:local_packages/@node-red/util"
  }

1.3. 書き換えた package.json の内容を node_modules に反映させる

$ npm install

以上で、node_modules 配下には local_packages からのハードリンクが張られる。
Node-RED Core 実体は local_packages にあるものが利用される。

→ コメント追加するなりして、自由にデバッグが出来るので HAPPY !!!

まとめ

本手順で作ったソース一式を git で管理し、herokuで好きなようにデバッグ出来るようになった。

→ その後の調査でherokuとは関係なく発生する問題であることが判明した。

続く...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む