- 投稿日:2020-09-21T23:56:12+09:00
アマゾンのセール情報からASINを取得する
アマゾンのセール情報からASINを取得
以前とは違ってアマゾンのセール情報からASINを取得してみよう。セール情報から取得する場合はアマゾンが作ったセール用のnode番号が必要になる。
sync-requestとjsdomを使っていたが、今回はrequestとjsdomを使う。
Amazonの検索結果からasinをスクレイピング
https://qiita.com/99nyorituryo/items/c5d53a3ca8a4967b5927ASINの取得
例として、「秋のKADOKAWAコミック&ラノベ フェア」のアドレスからASINをとる。
https://www.amazon.co.jp/b?ie=UTF8&node=8420570051上のページを開いてからkindle本、ページ2を選ぶ。なぜそうするかというと、ページ番号を可変させて、一括でASINを取得したいから。
下のアドレスから、page=2を探して2の部分を入れ替えながら、requestで取得する。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/
- 投稿日:2020-09-21T23:42:28+09:00
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.isArray
やArray.forEach
などの配列周りの関数JSON.parse
Date.now()
- getterとsetter
- 配列の最後の要素の後の余分なカンマ(←地味に嬉しい)
などがあるようです。だいぶモダンみが増してきました。ES5は全てのモダンなブラウザがサポートする最も高いバージョンです。というのは、IE9が
"use strict"
に対応していない、というのがあるみたいです。ECMAScript6 (2015)
いわゆる
ES6
ですね。このバージョンから、バージョン番号は西暦の下1桁+1になっててややこしいです。
let
とconst
- 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.json
のbabel
セクション- プロジェクトの一部(のディレクトリ?)に適用したいとき
.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
- 投稿日:2020-09-21T20:31:50+09:00
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.json
のentities
~subscribers
のセクションを以下のように出力先の.js
ファイルを参照する様に書き換える"entities": [ "dist/entity/**/*.js" ], "migrations": [ "dist/migration/**/*.js" ], "subscribers": [ "dist/subscriber/**/*.js" ],
- 投稿日:2020-09-21T20:04:10+09:00
Node.jsでsubmitボタンをFont Awswsomeにする方法
プログラミング勉強日記
2020年9月21日
Node.jsのejsファイルでformでデータを送るときにFont Awesomeを使おうとするときに試行錯誤したので備忘録として書いておく。現在と目標
現在<form action="/delete/<%= user.id %>/<%= users[0].id %>" method="post"> <input type="submit" value="削除"> </form>それを以下のようにFont Awesomeを用いてアイコンにしたい。
解決方法
解決方法<form action="/delete/<%= user.id %>/<%= users[0].id %>" method="post"> <button type="submit" class="btn"><i class="fas fa-trash-alt"></i></button> </form>個人的にはアイコンの周りの線(ボタンの部分)を消したいが、うまくできないのでこれがうまくできたらまた記事にしようと思う。
- 投稿日:2020-09-21T20:04:10+09:00
Node.jsでsubmitボタンをFont Awesomeにする方法
プログラミング勉強日記
2020年9月21日
Node.jsのejsファイルでformでデータを送るときにFont Awesomeを使おうとするときに試行錯誤したので備忘録として書いておく。現在と目標
現在<form action="/delete/<%= user.id %>/<%= users[0].id %>" method="post"> <input type="submit" value="削除"> </form>それを以下のようにFont Awesomeを用いてアイコンにしたい。
解決方法
解決方法<form action="/delete/<%= user.id %>/<%= users[0].id %>" method="post"> <button type="submit" class="btn"><i class="fas fa-trash-alt"></i></button> </form>個人的にはアイコンの周りの線(ボタンの部分)を消したいが、うまくできないのでこれがうまくできたらまた記事にしようと思う。
- 投稿日:2020-09-21T17:02:24+09:00
JavaScript エラー処理 完全ガイド
本記事は、Valentino Gagliardi 氏の "A mostly complete guide to error handling in JavaScript." を許可を頂いた上で翻訳したものです。
TOC
- プログラミングにおけるエラーとは?
- JavaScript におけるエラーとは?
- JavaScript エラー型の種類
- 例外とは?
- 例外を投げると何が起きる?
- 同期的エラー処理
- 非同期エラー処理
- Node.js のエラー処理
- まとめ
プログラミングにおけるエラーとは?
私たちの書くプログラムは 常にうまく動作するわけではありません。
時に、プログラムを停止させたり、ユーザーに何か問題が起こったことを知らせたい シチュエーションがあります。
例えば、以下のようなケースがあるでしょう:
- プログラムが存在しないファイルを開こうとした
- ネットワークの接続が不調である
- ユーザーが無効な値を入力した
すべてのケースで、私たちがプログラマーとして、またはプログラミングエンジンを通して、 エラー を作成します。
エラーを作成することで、ユーザーに問題が起きたことをメッセージで伝えたり、プログラムの実行を停止させたりできるのです。
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 は上記のプロパティの他に、
columnNumber
、filename
、lineNumber
といった非標準プロパティを実装しています。JavaScript エラー型の種類
JavaScript にはたくさんのエラー型があります。具体的には以下の通りです:
Error
EvalError
InternalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
これらのエラー型は、あたらしいエラーオブジェクトを返す 本物のコンストラクタ関数 であることを忘れないでください。
あなた自身のエラーオブジェクトを作成する際、
Error
とTypeError
という最も一般的な 2 つのエラー型を使うことが多いでしょう。エラーの大多数は
InternalError
やSyntaxError
のように、JavaScript エンジンから直接的に発現するものがほとんどです。
TypeError
の一例は、const
に再代入しようとした際に発生します:const name = "Jules"; name = "Caty"; // TypeError: Assignment to constant variable.
SyntaxError
の一例は、タイプミスをしたときに発生します:va x = '33'; // SyntaxError: Unexpected identifierまたは、
await
をasync
関数以外で利用するなど、予約語を不適切な場所を使った場合にも発生します: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.message
やerror.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 要素には、
onlick
、onmouseenter
、onchange
など多くのイベントハンドラがあります。そのなかには、
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); });このパターンは、画像やスクリプトなどのリソースに欠損があった際に、代替となるリソースをローディングしたい場合 に便利です。
だたし、
onerror
はthrow
やtry/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.reject
とPromise.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 stringPromise において、
catch
はエラーを処理するための構成要素です。
catch
やthen
に加え、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 を返す ように仕向けることができるようになります。これはつまり、then
やcatch
、finally
といったチェーンが使えることを意味します: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 undefinedNode.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 つの基本的なメソッド持っています:on
とemit
です。以下のような単純な 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 つのイベントを待ち受けています: listening と connection です。
これらのイベントに加えて、イベントエミッターはエラーが発生した際に エラー イベントも発火します。
もしこの上記コードのポート番号を 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/await
のtry/catch
を使って例外を処理することをより簡単にします。このガイドを読んだ後は、 プログラムで起こり得るすべての状況を認識して、例外を正しくキャッチすることができる ようになっているはずです。
最後までお読み頂きありがとうございました!
- 投稿日:2020-09-21T17:02:24+09:00
【JavaScript】エラー処理 完全ガイド【保存版】
本記事は、Valentino Gagliardi 氏の "A mostly complete guide to error handling in JavaScript." を許可を頂いた上で翻訳したものです。
TOC
- プログラミングにおけるエラーとは?
- JavaScript におけるエラーとは?
- JavaScript エラー型の種類
- 例外とは?
- 例外を投げると何が起きる?
- 同期的エラー処理
- 非同期エラー処理
- Node.js のエラー処理
- まとめ
プログラミングにおけるエラーとは?
私たちの書くプログラムは 常にうまく動作するわけではありません。
時に、プログラムを停止させたり、ユーザーに何か問題が起こったことを知らせたい シチュエーションがあります。
例えば、以下のようなケースがあるでしょう:
- プログラムが存在しないファイルを開こうとした
- ネットワークの接続が不調である
- ユーザーが無効な値を入力した
すべてのケースで、私たちがプログラマーとして、またはプログラミングエンジンを通して、 エラー を作成します。
エラーを作成することで、ユーザーに問題が起きたことをメッセージで伝えたり、プログラムの実行を停止させたりできるのです。
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 は上記のプロパティの他に、
columnNumber
、filename
、lineNumber
といった非標準プロパティを実装しています。JavaScript エラー型の種類
JavaScript にはたくさんのエラー型があります。具体的には以下の通りです:
Error
EvalError
InternalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
これらのエラー型は、あたらしいエラーオブジェクトを返す 本物のコンストラクタ関数 であることを忘れないでください。
あなた自身のエラーオブジェクトを作成する際、
Error
とTypeError
という最も一般的な 2 つのエラー型を使うことが多いでしょう。エラーの大多数は
InternalError
やSyntaxError
のように、JavaScript エンジンから直接的に発現するものがほとんどです。
TypeError
の一例は、const
に再代入しようとした際に発生します:const name = "Jules"; name = "Caty"; // TypeError: Assignment to constant variable.
SyntaxError
の一例は、タイプミスをしたときに発生します:va x = '33'; // SyntaxError: Unexpected identifierまたは、
await
をasync
関数以外で利用するなど、予約語を不適切な場所を使った場合にも発生します: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.message
やerror.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 要素には、
onlick
、onmouseenter
、onchange
など多くのイベントハンドラがあります。そのなかには、
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); });このパターンは、画像やスクリプトなどのリソースに欠損があった際に、代替となるリソースをローディングしたい場合 に便利です。
だたし、
onerror
はthrow
やtry/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.reject
とPromise.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 stringPromise において、
catch
はエラーを処理するための構成要素です。
catch
やthen
に加え、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 を返す ように仕向けることができるようになります。これはつまり、then
やcatch
、finally
といったチェーンが使えることを意味します: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 undefinedNode.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 つの基本的なメソッド持っています:on
とemit
です。以下のような単純な 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 つのイベントを待ち受けています: listening と connection です。
これらのイベントに加えて、イベントエミッターはエラーが発生した際に エラー イベントも発火します。
もしこの上記コードのポート番号を 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/await
のtry/catch
を使って例外を処理することをより簡単にします。このガイドを読んだ後は、 プログラムで起こり得るすべての状況を認識して、例外を正しくキャッチすることができる ようになっているはずです。
最後までお読み頂きありがとうございました!
- 投稿日:2020-09-21T15:44:53+09:00
【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参考
https://stackoverflow.com/questions/53545800/internal-modules-cjs-loader-js582-throw-err
https://qiita.com/TakuTaku04/items/02ff2f4555f705e8c055
- 投稿日:2020-09-21T11:57:58+09:00
【Bitcoinを自動売買】AWSのDockerで運用してみた話、外出先のAppleWatchから1TAPでON/OFF&Line通知付き
autocoin2
BitCoinのFX自動売買プログラム。
BitflyerのAPIを利用して、node.jsにて仮想通貨トレードを自動化しました。
寝てる時、トイレ中、24時間中、お金が勝手に増えてくれたら、どんなに素敵だろう。。
楽して自動的に儲かりたい!そんなダメ人間モチベーションで作ってみました。
いきなり結論ですが、、、残念ながら儲かりません
むしろ、減っています。。ですが、チューニングしたら、ひょっとしたら儲かり出すかもしれません。
(損害を受けても当方は一切責任はありません。)
あくまで、自己責任でお願いします!特徴
- 売り・買いポジション両方対応
- 複数アルゴリズムによる重み付け売買判断
- MongoDBによる売買履歴の保存
- 取引開始をLine通知
- 損得金額の閾値を超えたら、Lineにて通知
- 一定の日数が経過したら、ポジションを自動で手放す機能
- 日付変更30分前には、新たなポジション取得を抑制する機能
- Apple Home連携で外出先でもiphoneから1タップでON/OFF
- プログラム稼働中でも、並行して通常の人的トレードも可能
システム概要
使用技術
- 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できるようにしました。
- AWS EC2インスタンスにDockerを展開
- 自宅のRaspberry Piにhomebridge展開し、実行のshellファイルを紐付ける
詳しくは以下を参考ください。
公式homebridgePhilips_HueをAPI連動!〜ラズパイをHomeKit化する
IntelliJで取引DBを見る
MongoDBに保存した取引内容ですが、閲覧はIDEの利用をおすすめします。
直接MongoDBからの閲覧は、json形式のためかなり辛いです。
IntelliJなどのIDEでしたら、いい感じに見やすくしてくれます。
IntelliJの設定方法は、過去記事を参照ください。
IntelliJからAWSを操作する設定方法まとめ注意点
最初のDocker立ち上げ
MongoDBが書き込み可能になるのに時間かかります。
volumeディレクトリのcontainer_dataを作成するためです。
余裕持って初回起動して、しばらくしても書き込みされなければ、再度Dockerを立ち上げ直してください。
私の場合ですが、2回目以降は問題なく稼働しています。Docker Volumeのcontainer_dataはroot権限
作成されるvolumeディレクトリのcontainer_dataはroot権限で作られます。削除したい際はsudoを付けてください。
私のうっかり経験ですが、Dockerを再ビルドする際に、このディレクトリ権限に気付かずエラーになって少しハマりました。終わりに
楽して不労所得の夢は実現しません
しつこいですが、必ず儲かるわけではないですし、自己責任でお願いします。ひょっとしたら、
私のアルゴリズムや、パラメーターがイマイチナだけで、誰かが追加したアルゴリズムにより儲かりだすかもしれません。。。そんな時は、こっそりと教えて下さいね
それでは、よろしければホビーとしてほどほどに楽しんでみてください。
- 投稿日:2020-09-21T02:29:06+09:00
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 ./data1. 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.json1.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とは関係なく発生する問題であることが判明した。
続く...