├── .eslintignore ├── .npmignore ├── .flowconfig ├── coverageconfig.json ├── .gitignore ├── docs ├── assets │ └── exceptions │ │ ├── flow1.png │ │ ├── flow2.png │ │ ├── flow3.png │ │ └── flow4.png ├── promise-vs-task-api.md ├── api-reference.md └── exceptions.md ├── .travis.yml ├── examples └── io │ ├── README.md │ └── 1.js ├── test ├── concat.js ├── ap.js ├── empty.js ├── rejected.js ├── of.js ├── bimap.js ├── common.js ├── do.js ├── map.js ├── mapRejected.js ├── toString.js ├── race.js ├── parallel.js ├── fromComputatino.js ├── chain.js └── orElse.js ├── .eslintrc ├── LICENSE ├── package.json ├── README.md └── src └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | lib-es 3 | umd 4 | .build-artefacts 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | .nyc_output 3 | .build-artefacts 4 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | -------------------------------------------------------------------------------- /coverageconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage": ["./.build-artefacts/lcov.info"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | lib-es 4 | umd 5 | npm-debug.log 6 | .nyc_output 7 | .build-artefacts 8 | -------------------------------------------------------------------------------- /docs/assets/exceptions/flow1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/fun-task/master/docs/assets/exceptions/flow1.png -------------------------------------------------------------------------------- /docs/assets/exceptions/flow2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/fun-task/master/docs/assets/exceptions/flow2.png -------------------------------------------------------------------------------- /docs/assets/exceptions/flow3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/fun-task/master/docs/assets/exceptions/flow3.png -------------------------------------------------------------------------------- /docs/assets/exceptions/flow4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/fun-task/master/docs/assets/exceptions/flow4.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '5.10' 5 | cache: 6 | directories: 7 | - node_modules 8 | script: 9 | - npm test 10 | - npm run lobot test coveralls || true 11 | -------------------------------------------------------------------------------- /examples/io/README.md: -------------------------------------------------------------------------------- 1 | This shows rather unusual use of Task. Task is used to write synchronous but impure code in pure fashion. 2 | 3 | ```sh 4 | # from project root 5 | npm run lobot run ./examples/io/1.js 6 | ``` 7 | -------------------------------------------------------------------------------- /test/concat.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('concat') 7 | 8 | test('works', 1, t => { 9 | Task.of(1).concat(Task.of(2)).run(t.calledWith(1)) 10 | }) 11 | 12 | test('static alias works', 1, t => { 13 | Task.concat(Task.of(1), Task.of(2)).run(t.calledWith(1)) 14 | }) 15 | -------------------------------------------------------------------------------- /test/ap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('ap') 7 | 8 | test('works', 1, t => { 9 | Task.of(x => x + 1).ap(Task.of(2)).run(t.calledWith(3)) 10 | }) 11 | 12 | test('static alias works', 1, t => { 13 | Task.ap(Task.of(x => x + 1), Task.of(2)).run(t.calledWith(3)) 14 | }) 15 | -------------------------------------------------------------------------------- /test/empty.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('empty') 7 | 8 | test('doens\'t call cbs', 0, t => { 9 | Task.empty().run({success: t.fail, failure: t.fail}) 10 | }) 11 | 12 | test('instance method works', 0, t => { 13 | Task.of(1).empty().run({success: t.fail, failure: t.fail}) 14 | }) 15 | -------------------------------------------------------------------------------- /test/rejected.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('rejected') 7 | 8 | test('passes value to cb', 1, t => { 9 | Task.rejected(2).run({success: t.fail, failure: t.calledWith(2)}) 10 | }) 11 | 12 | test('this==undefined in cd', 1, t => { 13 | Task.rejected(2).run({failure() { t.equal(this, undefined) }}) 14 | }) 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["eslint:recommended"], 4 | "plugins": [ 5 | "flowtype" 6 | ], 7 | "rules": { 8 | "flowtype/define-flow-type": 1, 9 | "flowtype/type-id-match": [1, "^([A-Z][a-z0-9]+)+$"], 10 | "flowtype/use-flow-type": 1, 11 | "comma-dangle": [2, 'always-multiline'] 12 | }, 13 | "env": { 14 | "node": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/of.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('of') 7 | 8 | test('passes value to cb', 1, t => { 9 | Task.of(2).run(t.calledWith(2)) 10 | }) 11 | 12 | test('this==undefined in cd', 1, t => { 13 | Task.of(2).run(function() { t.equal(this, undefined) }) 14 | }) 15 | 16 | test('instance method works', 1, t => { 17 | Task.of(1).of(3).run(t.calledWith(3)) 18 | }) 19 | -------------------------------------------------------------------------------- /test/bimap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('bimap') 7 | 8 | test('works with of', 1, t => { 9 | Task.of(2).bimap(x => x, x => x + 10).run(t.calledWith(12)) 10 | }) 11 | 12 | test('works with .rejected', 1, t => { 13 | Task.rejected(2).bimap(x => x + 10, x => x).run({failure: t.calledWith(12)}) 14 | }) 15 | 16 | test('static alias works', 2, t => { 17 | Task.bimap(x => x, x => x + 10, Task.of(2)).run(t.calledWith(12)) 18 | Task.bimap(x => x + 10, x => x, Task.rejected(2)).run({failure: t.calledWith(12)}) 19 | }) 20 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('common') 7 | 8 | test('new Task() throws', 1, t => { 9 | t.throws(() => new Task(), /Task\.create/) 10 | }) 11 | 12 | test('default onFail cb in run works', 1, t => { 13 | t.throws(() => Task.rejected('err1').run({}), /err1/) 14 | }) 15 | 16 | test('default onFail cb throws the same Error if argument is an Error', 1, t => { 17 | const e = new Error('') 18 | try { 19 | Task.rejected(e).run({}) 20 | } catch (_e) { 21 | t.ok(e === _e) 22 | } 23 | }) 24 | 25 | test('runAndLog works (success)', 0, () => { 26 | // something goes sideways if we try to mock console, 27 | // so we just check that method exists and runs fine 28 | Task.of(2).runAndLog() 29 | }) 30 | 31 | test('runAndLog works (failure)', 0, () => { 32 | // same deal... 33 | Task.rejected(2).runAndLog() 34 | }) 35 | -------------------------------------------------------------------------------- /test/do.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | import 'babel-polyfill' 7 | 8 | const test = _test.wrap('do') 9 | 10 | test('works in simpliest case', 1, t => { 11 | Task.do(function* () {return Task.of(2)}).run(t.calledWith(2)) 12 | }) 13 | 14 | test('works in simpliest case (rejected)', 1, t => { 15 | Task.do(function* () {return Task.rejected(2)}).run({failure: t.calledWith(2)}) 16 | }) 17 | 18 | test('part of generator after `yield rejected` is not executed', 1, t => { 19 | Task.do(function* () { 20 | yield Task.rejected(2) 21 | t.fail() 22 | return Task.of(1) 23 | }).run({failure: t.calledWith(2)}) 24 | }) 25 | 26 | test('works in next to simpliest case', 1, t => { 27 | Task.do(function* () { 28 | const x: any = yield Task.of(2) 29 | const y: any = yield Task.of(3) 30 | return Task.of(x * y) 31 | }).run(t.calledWith(6)) 32 | }) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Roman Pominov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fun-task", 3 | "version": "1.1.2", 4 | "description": "An abstraction for managing asynchronous code in JS", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "lobot": "lobot", 8 | "lint": "eslint .", 9 | "test": "eslint . && flow check && lobot test", 10 | "preversion": "cp src/index.js lib/index.js.flow" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/rpominov/fun-task.git" 15 | }, 16 | "author": "Roman Pominov ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/rpominov/fun-task/issues" 20 | }, 21 | "homepage": "https://github.com/rpominov/fun-task#readme", 22 | "devDependencies": { 23 | "babel-eslint": "6.1.2", 24 | "babel-polyfill": "6.13.0", 25 | "eslint": "2.13.1", 26 | "eslint-plugin-flowtype": "2.7.0", 27 | "flow-bin": "0.30.0", 28 | "lobot": "0.1.19" 29 | }, 30 | "jsnext:main": "lib-es/index.js", 31 | "files": [ 32 | "lib", 33 | "lib-es", 34 | "umd" 35 | ], 36 | "dependencies": { 37 | "static-land": "0.1.7" 38 | }, 39 | "keywords": [ 40 | "task", 41 | "future", 42 | "promise", 43 | "monad", 44 | "applicative", 45 | "functor", 46 | "fantasy-land", 47 | "static-land", 48 | "fantasy land", 49 | "static land", 50 | "fp", 51 | "functional" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /test/map.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('map') 7 | 8 | test('works with of', 1, t => { 9 | Task.of(2).map(x => x + 10).run(t.calledWith(12)) 10 | }) 11 | 12 | test('static alias works', 1, t => { 13 | Task.map(x => x + 10, Task.of(2)).run(t.calledWith(12)) 14 | }) 15 | 16 | test('this==undefined in success cd', 1, t => { 17 | Task.of(2).map(x => x).run({success() { t.equal(this, undefined) }}) 18 | }) 19 | 20 | test('this==undefined in failure cd', 1, t => { 21 | Task.rejected(2).map(x => x).run({failure() { t.equal(this, undefined) }}) 22 | }) 23 | 24 | test('this==undefined in fn', 1, t => { 25 | Task.of(2).map(function(x) { t.equal(this, undefined); return x }).run({}) 26 | }) 27 | 28 | const thrower1 = Task.create(() => { throw new Error('err1') }) 29 | const thrower2 = Task.create(() => { throw 2 }) 30 | 31 | test('exception thrown from parent task (no catch cb)', 1, t => { 32 | t.throws(() => { 33 | thrower1.map(x => x).run({}) 34 | }, /err1/) 35 | }) 36 | 37 | test('exception thrown from parent task (with catch cb)', 1, t => { 38 | thrower2.map(x => x).run({catch: t.calledWith(2)}) 39 | }) 40 | 41 | test('exception thrown from fn (no catch cb)', 1, t => { 42 | t.throws(() => { 43 | Task.of(1).map(() => { throw new Error('err1') }).run({}) 44 | }, /err1/) 45 | }) 46 | 47 | test('exception thrown from fn (with catch cb)', 1, t => { 48 | Task.of(1).map(() => { throw 2 }).run({catch: t.calledWith(2)}) 49 | }) 50 | -------------------------------------------------------------------------------- /test/mapRejected.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('mapRejected') 7 | 8 | test('works with .rejected', 1, t => { 9 | Task.rejected(2).mapRejected(x => x + 10).run({failure: t.calledWith(12)}) 10 | }) 11 | 12 | test('static alias works', 1, t => { 13 | Task.mapRejected(x => x + 10, Task.rejected(2)).run({failure: t.calledWith(12)}) 14 | }) 15 | 16 | test('this==undefined in success cd', 1, t => { 17 | Task.of(2).mapRejected(x => x).run({success() { t.equal(this, undefined) }}) 18 | }) 19 | 20 | test('this==undefined in failure cd', 1, t => { 21 | Task.rejected(2).mapRejected(x => x).run({failure() { t.equal(this, undefined) }}) 22 | }) 23 | 24 | test('this==undefined in fn', 1, t => { 25 | Task.rejected(2).mapRejected(function(x) { t.equal(this, undefined); return x }).run({failure(){}}) 26 | }) 27 | 28 | const thrower1 = Task.create(() => { throw new Error('err1') }) 29 | const thrower2 = Task.create(() => { throw 2 }) 30 | 31 | test('exception thrown from parent task (no catch cb)', 1, t => { 32 | t.throws(() => { 33 | thrower1.mapRejected(x => x).run({}) 34 | }, /err1/) 35 | }) 36 | 37 | test('exception thrown from parent task (with catch cb)', 1, t => { 38 | thrower2.mapRejected(x => x).run({catch: t.calledWith(2)}) 39 | }) 40 | 41 | test('exception thrown from fn (no catch cb)', 1, t => { 42 | t.throws(() => { 43 | Task.rejected(1).mapRejected(() => { throw new Error('err1') }).run({}) 44 | }, /err1/) 45 | }) 46 | 47 | test('exception thrown from fn (with catch cb)', 1, t => { 48 | Task.rejected(1).mapRejected(() => { throw 2 }).run({catch: t.calledWith(2)}) 49 | }) 50 | -------------------------------------------------------------------------------- /examples/io/1.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import readline from 'readline' 4 | import Task from '../../src' 5 | 6 | type Empty = void & null 7 | const fixEmpty = (x: Empty | T): T => (x: any) 8 | 9 | // wrappers 10 | 11 | // this is actually async, I didn't figured out how to read sync from stdin in Node 12 | const read = (): Task => Task.create(suc => { 13 | const rl = readline.createInterface({input: process.stdin}) 14 | rl.on('line', line => { rl.close(); suc(line) }) 15 | return () => { rl.close() } 16 | }) 17 | 18 | const write = (text: string): Task => Task.create(suc => { 19 | console.log(text) // eslint-disable-line 20 | suc() 21 | }) 22 | 23 | 24 | // pure 25 | 26 | const strToNumber = (str: string): Task => /^\d+$/.test(str) 27 | ? Task.of(Number(str)) 28 | : Task.rejected('That\'s not a number') 29 | 30 | // This could be in the library 31 | const retryUntilSuccess = (task: Task): Task => { 32 | const recur = () => task.orElse(recur) 33 | return recur() 34 | } 35 | 36 | // This could be in the library (like all() but not parallel) 37 | const sequentially = (task1: Task, task2: Task): Task<[S1, S2], F1 | F2> => 38 | task1.chain(x1 => task2.map(x2 => [x1, x2])) 39 | 40 | const getNumber = (message: string): Task => 41 | retryUntilSuccess( 42 | write(message) 43 | .chain(read) 44 | .chain(strToNumber) 45 | .orElse(error => write(fixEmpty(error)).chain(Task.rejected)) 46 | ) 47 | 48 | const program: Task = 49 | sequentially(getNumber('Give me a number'), getNumber('Give me another number')) 50 | .map(([x, y]) => `${x} * ${y} = ${x * y}`) 51 | .chain(write) 52 | 53 | 54 | // impure 55 | 56 | program.run({}) 57 | -------------------------------------------------------------------------------- /test/toString.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('toString') 7 | 8 | test('of', 1, t => { 9 | t.equals(Task.of(1).toString(), 'Task.of(1)') 10 | }) 11 | 12 | test('rejected', 1, t => { 13 | t.equals(Task.rejected(1).toString(), 'Task.rejected(1)') 14 | }) 15 | 16 | test('create', 1, t => { 17 | t.equals(Task.create(() => {}).toString(), 'Task.create(..)') 18 | }) 19 | 20 | test('empty', 1, t => { 21 | t.equals(Task.empty().toString(), 'Task.empty()') 22 | }) 23 | 24 | test('parallel', 1, t => { 25 | t.equals(Task.parallel([Task.empty(), Task.of(1)]).toString(), 'Task.parallel([empty(), of(1)])') 26 | }) 27 | 28 | test('race', 1, t => { 29 | t.equals(Task.race([Task.empty(), Task.of(1)]).toString(), 'Task.race([empty(), of(1)])') 30 | }) 31 | 32 | test('concat', 1, t => { 33 | t.equals(Task.empty().concat(Task.of(1)).toString(), 'Task.race([empty(), of(1)])') 34 | }) 35 | 36 | test('map', 1, t => { 37 | t.equals(Task.of(1).map(x => x).toString(), 'Task.of(1).map(..)') 38 | }) 39 | 40 | test('map', 1, t => { 41 | t.equals(Task.of(1).bimap(x => x, x => x).toString(), 'Task.of(1).map(..).mapRejected(..)') 42 | }) 43 | 44 | test('mapRejected', 1, t => { 45 | t.equals(Task.of(1).mapRejected(x => x).toString(), 'Task.of(1).mapRejected(..)') 46 | }) 47 | 48 | test('chain', 1, t => { 49 | t.equals(Task.of(1).chain(x => Task.of(x)).toString(), 'Task.of(1).chain(..)') 50 | }) 51 | 52 | test('orElse', 1, t => { 53 | t.equals(Task.rejected(1).orElse(x => Task.of(x)).toString(), 'Task.rejected(1).orElse(..)') 54 | }) 55 | 56 | test('ap', 1, t => { 57 | const str = Task.of(x => x).ap(Task.of(1)).toString() 58 | t.ok(/^Task\.of\([\s\S]+\)\.chain\(\.\.\)$/.test(str), `String "${str}" doesn't match regex`) 59 | }) 60 | -------------------------------------------------------------------------------- /test/race.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('race') 7 | 8 | test('works with of', 1, t => { 9 | Task.race([Task.of(2), Task.of(3), Task.rejected(4)]).run({success: t.calledWith(2), failure: t.fail}) 10 | }) 11 | 12 | test('works with rejected', 1, t => { 13 | Task.race([Task.rejected(2), Task.of(3), Task.rejected(4)]).run({success: t.fail, failure: t.calledWith(2)}) 14 | }) 15 | 16 | test('cancelation works', 2, t => { 17 | Task.race([ 18 | Task.create(() => t.calledOnce()), 19 | Task.create(() => t.calledOnce()), 20 | ]).run({})() 21 | }) 22 | 23 | test('after one task comletes others a canceled', 1, t => { 24 | Task.race([ 25 | Task.of(2), 26 | Task.create(() => t.calledOnce()), 27 | ]).run({}) 28 | }) 29 | 30 | test('after one task comletes others a canceled (async)', 1, t => { 31 | let s: any = null 32 | Task.race([ 33 | Task.create((_s) => {s = _s; return t.fail}), 34 | Task.create(() => t.calledOnce()), 35 | ]).run({}) 36 | s() 37 | }) 38 | 39 | const of1 = Task.of(1) 40 | const thrower1 = Task.create(() => { throw new Error('err1') }) 41 | const thrower2 = Task.create(() => { throw 2 }) 42 | 43 | test('exception thrown in a child task (no catch cb)', 1, t => { 44 | t.throws(() => { 45 | Task.race([thrower1, of1]).run({}) 46 | }, /err1/) 47 | }) 48 | 49 | test('exception thrown in a child task (with catch cb)', 1, t => { 50 | Task.race([thrower2, of1]).run({catch: t.calledWith(2), success: t.fail}) 51 | }) 52 | 53 | test('this==undefined in success cd', 1, t => { 54 | Task.race([Task.of(2)]).run({success() { t.equal(this, undefined) }}) 55 | }) 56 | 57 | test('this==undefined in failure cd', 1, t => { 58 | Task.race([Task.rejected(2)]).run({failure() { t.equal(this, undefined) }}) 59 | }) 60 | -------------------------------------------------------------------------------- /test/parallel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('parallel') 7 | 8 | test('works with of', 1, t => { 9 | Task.parallel([Task.of(2), Task.of('42')]).run(t.calledWith([2, '42'])) 10 | }) 11 | 12 | test('works with rejected', 1, t => { 13 | Task.parallel([Task.of(2), Task.rejected('42')]).run({failure: t.calledWith('42')}) 14 | }) 15 | 16 | test('cancelation works', 2, t => { 17 | Task.parallel([ 18 | Task.create(() => t.calledOnce()), 19 | Task.create(() => t.calledOnce()), 20 | ]).run({})() 21 | }) 22 | 23 | test('after one task fails others are canceled (sync fail)', 1, t => { 24 | Task.parallel([ 25 | Task.rejected(2), 26 | Task.create(() => t.calledOnce()), 27 | ]).run({failure(){}}) 28 | }) 29 | 30 | test('after one task fails others are canceled (async fail)', 1, t => { 31 | let f = (null: any) 32 | Task.parallel([ 33 | Task.create((_, _f) => { f = _f }), 34 | Task.create(() => t.calledOnce()), 35 | ]).run({failure(){}}) 36 | f() 37 | }) 38 | 39 | const of1 = Task.of(1) 40 | const thrower1 = Task.create(() => { throw new Error('err1') }) 41 | const thrower2 = Task.create(() => { throw 2 }) 42 | 43 | test('exception thrown in a child task (no catch cb)', 2, t => { 44 | t.throws(() => { 45 | Task.parallel([of1, thrower1]).run({}) 46 | }, /err1/) 47 | t.throws(() => { 48 | Task.parallel([thrower1, of1]).run({}) 49 | }, /err1/) 50 | }) 51 | 52 | test('exception thrown in a child task (with catch cb, exception is the first completion)', 1, t => { 53 | Task.parallel([thrower2, of1]).run({catch: t.calledWith(2), success: t.fail}) 54 | }) 55 | 56 | test('exception thrown in a child task (with catch cb, exception is the second completion)', 1, t => { 57 | Task.parallel([of1, thrower2]).run({catch: t.calledWith(2), success: t.fail}) 58 | }) 59 | 60 | test('this==undefined in success cd', 1, t => { 61 | Task.parallel([Task.of(2)]).run({success() { t.equal(this, undefined) }}) 62 | }) 63 | 64 | test('this==undefined in failure cd', 1, t => { 65 | Task.parallel([Task.rejected(2)]).run({failure() { t.equal(this, undefined) }}) 66 | }) 67 | -------------------------------------------------------------------------------- /docs/promise-vs-task-api.md: -------------------------------------------------------------------------------- 1 | # API comparison with Promises 2 | 3 | | Task | Promise & comments | 4 | | ---------------------------------------- | ---------------------------------------- | 5 | | `Task.create(computation)` | `new Promise(computation)` | 6 | | `Task.of(x)` | `Promise.resolve(x)`

With Promises behaviour is different if `x` is a Promise (this makes writing generic code more difficult with Promises) | 7 | | `Task.rejected(x)` | `Promise.reject(x)` | 8 | | `task.map(fn)` | `promise.then(fn)`

With Promises behaviour is different if `fn` retruns a Promise | 9 | | `task.chain(fn)` | `promise.then(fn)` | 10 | | `task.mapRejected(fn)` | `promise.then(undefined, fn)`

With Promises behaviour is different if `fn` retruns a Promise | 11 | | `task.orElse(fn)` | `promise.then(undefined, fn)` | 12 | | `task.ap(otherTask)` | `Promise.all(promise, otherPromise).then(([fn, x]) => fn(x))`

This method exists mainly for compliance with [Fantasy Land Specification](https://github.com/fantasyland/fantasy-land) | 13 | | `Task.empty()` | `new Promise(() => {})` | 14 | | `task.concat(otherTask)` | `Promise.race([promose, otherPromise])`

Aslo mainly for Fantasy Land, makes Task a [Monoid](https://github.com/fantasyland/fantasy-land#monoid) | 15 | | `Task.parallel(tasks)` | `Promise.all(promises)` | 16 | | `Task.race(tasks)` | `Promise.race(promises)` | 17 | | `Task.run({success, failure})` | `Promise.then(success, failure)` | 18 | | `Task.run({success, failure, catch})` | `Promise.then(success, failureAndCatch)`

By default tasks don't catch exceptions thrown from `map`, `chain` etc. But we can choose to catch them by providing `catch` callback. Also notice that exceptions go into their own callback. | 19 | | `cancel = task.run(...); cancel()` | Promises don't support cancelation or even unsubscribing | 20 | -------------------------------------------------------------------------------- /test/fromComputatino.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('fromComputation') 7 | 8 | test('succ value from computation is passed to run() cb', 1, t => { 9 | Task.create(s => s(2)).run(t.calledWith(2)) 10 | }) 11 | 12 | test('cancelation cb returned by computation is called', 1, t => { 13 | Task.create(() => t.calledOnce()).run({})() 14 | }) 15 | 16 | test('cancelation cb returned by computation is called only once', 1, t => { 17 | const cancel = Task.create(() => t.calledOnce()).run({}) 18 | cancel() 19 | cancel() 20 | }) 21 | 22 | test('after a cuss, cancelation cb returned by computation isn\'t called', 0, t => { 23 | Task.create((s) => { s(1); return t.fail }).run({})() 24 | }) 25 | 26 | test('after a cuss, cancelation cb returned by computation isn\'t called (async)', 0, t => { 27 | let s: any = null 28 | const cancel = Task.create((_s) => { s = _s; return t.fail }).run({}) 29 | s() 30 | cancel() 31 | }) 32 | 33 | test('after a cuss, cancelation cb returned by computation isn\'t called (cancelation in success cb)', 0, t => { 34 | let s: any = null 35 | const cancel = Task.create((_s) => { s = _s; return t.fail }).run({ 36 | success() { 37 | cancel() 38 | }, 39 | }) 40 | s() 41 | }) 42 | 43 | test('after a cuss, all calls of computation cbs are ignored', 1, t => { 44 | let s = (null: any) 45 | let f = (null: any) 46 | const task = Task.create((_s, _f) => {s = _s; f = _f}) 47 | task.run({success: t.calledOnce(), failure: t.fail}) 48 | s() 49 | s() 50 | f() 51 | }) 52 | 53 | test('after a fail, all calls of computation cbs are ignored', 1, t => { 54 | let s = (null: any) 55 | let f = (null: any) 56 | const task = Task.create((_s, _f) => {s = _s; f = _f}) 57 | task.run({success: t.fail, failure: t.calledOnce()}) 58 | f() 59 | f() 60 | s() 61 | }) 62 | 63 | test('after cancelation, all calls of computation cbs are ignored', 0, t => { 64 | let s = (null: any) 65 | let f = (null: any) 66 | const task = Task.create((_s, _f) => {s = _s; f = _f}) 67 | task.run({success: t.fail, failure: t.fail})() 68 | s() 69 | f() 70 | }) 71 | 72 | test('exception thrown from computation (no catch cb)', 1, t => { 73 | t.throws(() => Task.create(() => { throw new Error('err1') }).run({}), /err1/) 74 | }) 75 | 76 | test('exception thrown from computation (with catch cb)', 1, t => { 77 | Task.create(() => { throw 2 }).run({catch: t.calledWith(2)}) 78 | }) 79 | 80 | test('this==undefined in success cb', 1, t => { 81 | Task.create(s => s(2)).run({success() { t.equal(this, undefined) }}) 82 | }) 83 | 84 | test('this==undefined in failure cb', 1, t => { 85 | Task.create((_, f) => f(2)).run({failure() { t.equal(this, undefined) }}) 86 | }) 87 | 88 | test('this==undefined in fn', 1, t => { 89 | Task.create(function() { t.equal(this, undefined) }).run({}) 90 | }) 91 | -------------------------------------------------------------------------------- /test/chain.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('chain') 7 | 8 | test('works with of + of', 1, t => { 9 | Task.of(2).chain(x => Task.of(x + 10)).run(t.calledWith(12)) 10 | }) 11 | 12 | test('static alias works', 1, t => { 13 | Task.chain(x => Task.of(x + 10), Task.of(2)).run(t.calledWith(12)) 14 | }) 15 | 16 | test('works with of + rejected', 1, t => { 17 | Task.of(2).chain(x => Task.rejected(x + 10)).run({failure: t.calledWith(12)}) 18 | }) 19 | 20 | test('works with rejected + of', 1, t => { 21 | Task.rejected(2).chain(x => Task.of(x + 10)).run({failure: t.calledWith(2)}) 22 | }) 23 | 24 | test('cancelation works (orig. task)', 1, t => { 25 | Task.create(() => t.calledOnce()).chain(() => Task.of()).run({})() 26 | }) 27 | 28 | test('cancelation works (spawned task)', 1, t => { 29 | Task.of().chain(() => Task.create(() => t.calledOnce())).run({})() 30 | }) 31 | 32 | test('exception thrown from fn (no catch cb)', 1, t => { 33 | t.throws(() => { 34 | Task.of().chain(() => { throw new Error('err1') }).run({}) 35 | }, /err1/) 36 | }) 37 | 38 | test('exception thrown from fn (with catch cb)', 1, t => { 39 | Task.of().chain(() => { throw 2 }).run({catch: t.calledWith(2)}) 40 | }) 41 | 42 | const thrower1 = Task.create(() => { throw new Error('err1') }) 43 | const thrower2 = Task.create(() => { throw 2 }) 44 | 45 | test('exception thrown from parent task (no catch cb)', 1, t => { 46 | t.throws(() => { 47 | thrower1.chain(() => Task.of()).run({}) 48 | }, /err1/) 49 | }) 50 | 51 | test('exception thrown from parent task (with catch cb)', 1, t => { 52 | thrower2.chain(() => Task.of()).run({catch: t.calledWith(2)}) 53 | }) 54 | 55 | test('exception thrown from child task (no catch cb)', 1, t => { 56 | t.throws(() => { 57 | Task.of().chain(() => thrower1).run({}) 58 | }, /err1/) 59 | }) 60 | 61 | test('exception thrown from child task (with catch cb)', 1, t => { 62 | Task.of().chain(() => thrower2).run({catch: t.calledWith(2)}) 63 | }) 64 | 65 | test('exception thrown from child task (with catch cb, async)', 1, t => { 66 | let s = (null: any) 67 | let thrower = Task.create(_s => { s = _s }).chain(() => { throw 2 }) 68 | Task.of().chain(() => thrower).run({catch: t.calledWith(2)}) 69 | s() 70 | }) 71 | 72 | test('exception thrown from child task (no catch cb, async)', 1, t => { 73 | let s = (null: any) 74 | let thrower = Task.create(_s => { s = _s }).chain(() => { throw new Error('err1') }) 75 | Task.of().chain(() => thrower).run({}) 76 | t.throws(s, /err1/) 77 | }) 78 | 79 | test('this==undefined in success cd', 1, t => { 80 | Task.of(2).chain(x => Task.of(x)).run({success() { t.equal(this, undefined) }}) 81 | }) 82 | 83 | test('this==undefined in failure cd', 1, t => { 84 | Task.rejected(2).chain(x => Task.of(x)).run({failure() { t.equal(this, undefined) }}) 85 | }) 86 | 87 | test('this==undefined in fn', 1, t => { 88 | Task.of(2).chain(function(x) { t.equal(this, undefined); return Task.of(x) }).run({}) 89 | }) 90 | -------------------------------------------------------------------------------- /test/orElse.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import _test from 'lobot/test' 4 | import Task from '../src' 5 | 6 | const test = _test.wrap('orElse') 7 | 8 | test('works with of + of', 1, t => { 9 | Task.of(2).orElse(x => Task.of(x + 10)).run(t.calledWith(2)) 10 | }) 11 | 12 | test('static alias works', 1, t => { 13 | Task.orElse(x => Task.of(x + 10), Task.of(2)).run(t.calledWith(2)) 14 | }) 15 | 16 | test('works with of + rejected', 1, t => { 17 | Task.of(2).orElse(x => Task.rejected(x + 10)).run(t.calledWith(2)) 18 | }) 19 | 20 | test('works with rejected + of', 1, t => { 21 | Task.rejected(2).orElse(x => Task.of(x + 10)).run(t.calledWith(12)) 22 | }) 23 | 24 | test('works with rejected + rejected', 1, t => { 25 | Task.rejected(2).orElse(x => Task.rejected(x + 10)).run({failure: t.calledWith(12)}) 26 | }) 27 | 28 | test('cancelation works (orig. task)', 1, t => { 29 | Task.create(() => t.calledOnce()).orElse(() => Task.of()).run({})() 30 | }) 31 | 32 | test('cancelation works (spawned task)', 1, t => { 33 | Task.rejected().orElse(() => Task.create(() => t.calledOnce())).run({})() 34 | }) 35 | 36 | test('exception thrown from fn (no catch cb)', 1, t => { 37 | t.throws(() => { 38 | Task.rejected().orElse(() => { throw new Error('err1') }).run({}) 39 | }, /err1/) 40 | }) 41 | 42 | test('exception thrown from fn (with catch cb)', 1, t => { 43 | Task.rejected().orElse(() => { throw 2 }).run({catch: t.calledWith(2)}) 44 | }) 45 | 46 | const thrower1 = Task.create(() => { throw new Error('err1') }) 47 | const thrower2 = Task.create(() => { throw 2 }) 48 | 49 | test('exception thrown from parent task (no catch cb)', 1, t => { 50 | t.throws(() => { 51 | thrower1.orElse(() => Task.of()).run({}) 52 | }, /err1/) 53 | }) 54 | 55 | test('exception thrown from parent task (with catch cb)', 1, t => { 56 | thrower2.orElse(() => Task.of()).run({catch: t.calledWith(2)}) 57 | }) 58 | 59 | test('exception thrown from child task (no catch cb)', 1, t => { 60 | t.throws(() => { 61 | Task.rejected().orElse(() => thrower1).run({}) 62 | }, /err1/) 63 | }) 64 | 65 | test('exception thrown from child task (with catch cb)', 1, t => { 66 | Task.rejected().orElse(() => thrower2).run({catch: t.calledWith(2)}) 67 | }) 68 | 69 | test('exception thrown from child task (with catch cb, async)', 1, t => { 70 | let f = (null: any) 71 | let thrower = Task.create((_, _f) => { f = _f }).orElse(() => { throw 2 }) 72 | Task.of().chain(() => thrower).run({catch: t.calledWith(2)}) 73 | f() 74 | }) 75 | 76 | test('exception thrown from child task (no catch cb, async)', 1, t => { 77 | let f = (null: any) 78 | let thrower = Task.create((_, _f) => { f = _f }).orElse(() => { throw new Error('err1') }) 79 | Task.of().chain(() => thrower).run({}) 80 | t.throws(f, /err1/) 81 | }) 82 | 83 | test('this==undefined in success cd', 1, t => { 84 | Task.of(2).chain(x => Task.of(x)).run({success() { t.equal(this, undefined) }}) 85 | }) 86 | 87 | test('this==undefined in success cd', 1, t => { 88 | Task.of(2).orElse(x => Task.rejected(x)).run({success() { t.equal(this, undefined) }}) 89 | }) 90 | 91 | test('this==undefined in failure cd', 1, t => { 92 | Task.rejected(2).orElse(x => Task.rejected(x)).run({failure() { t.equal(this, undefined) }}) 93 | }) 94 | 95 | test('this==undefined in fn', 1, t => { 96 | Task.rejected(2).orElse(function(x) { t.equal(this, undefined); return Task.of(x) }).run({}) 97 | }) 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fun-task* [![Build Status](https://travis-ci.org/rpominov/fun-task.svg?branch=master)](https://travis-ci.org/rpominov/fun-task) [![Coverage Status](https://coveralls.io/repos/github/rpominov/fun-task/badge.svg?branch=master)](https://coveralls.io/github/rpominov/fun-task?branch=master) 2 | 3 | An abstraction for managing asynchronous code in JS. 4 | 5 | \* The name is an abbreviation for "functional task" (this library is based on many ideas 6 | from Functional Programming). The type that library implements is usually referred as just Task in docs. 7 | 8 | 9 | ## Installation 10 | 11 | ### NPM 12 | 13 | ``` 14 | npm install fun-task 15 | ``` 16 | 17 | ```js 18 | // modern JavaScript 19 | import Task from 'fun-task' 20 | 21 | // classic JavaScript 22 | var Task = require('fun-task') 23 | ``` 24 | 25 | ### CDN 26 | 27 | ```html 28 | 29 | 32 | ``` 33 | 34 | 35 | ## What is a Task? 36 | 37 | Task is an abstraction similar to Promises. The key difference from Promises is that a 38 | Task represents a computation while a Promise represents only the result of a computation. 39 | Therefore if we have a Task we can: start the computation, terminate it before it finished, 40 | or wait until it finishes and get the result. While with a Promise we can only get the result. 41 | This difference don't make Tasks **better**, they are just different from Promises and we can 42 | find legitimate use cases for both abstractions. Let's review it again: 43 | 44 | If we have a Task: 45 | 46 | - We can start the computation that it represents (e.g. a network request) 47 | - We can choose not to start the computation and just throw task away 48 | - We can start it more than once 49 | - While computation is running we can notify it that we don't interested in the result any more, 50 | and as a response computation can choose to terminate itself 51 | - When computation finishes we get the result 52 | 53 | If we have a Promise: 54 | 55 | - Computation is already running (or finished) and we don't have any control of it 56 | - We can get the result whenever it's ready 57 | - If two or more consumers have a same Promise they all will get the same result 58 | 59 | The last item is important. This is the key advantage of Promises over Tasks. 60 | Tasks don't have this feature. If two consumers have a same Task, each of them have to spawn 61 | their own instance of the computation in order to get the result, 62 | and they may even get different results. 63 | 64 | 65 | ## What is a computation? 66 | 67 | ```js 68 | function computation(onSuccess, onFailure) { 69 | // ... 70 | return () => { 71 | // ... cancellation logic 72 | } 73 | } 74 | ``` 75 | 76 | From Task API perspective computation is just a function that accepts two callbacks. 77 | It should call one of them after completion with the final result. 78 | Also a computation may return a function with cancellation logic or it can return `undefined` 79 | if particular computation has no cancellation logic. 80 | 81 | Creating a Task from a computation is easy, we just call `task = Task.create(computation)`. 82 | This is very similar to `new Promise(computation)`, but Task won't call `computation` 83 | immediately, the computation starts only when `task.run()` is called 84 | (unlike with Promises where computation is started immediately). 85 | 86 | 87 | ## Documentation 88 | 89 | - [API reference](docs/api-reference.md) 90 | - [How exceptions catching work in Task](docs/exceptions.md#how-exceptions-work-in-task) 91 | - [API comparison with Promises](docs/promise-vs-task-api.md) 92 | 93 | ## [Flow](https://flowtype.org/) 94 | 95 | The NPM package ships with Flow definitions. So you can do something like this if you use Flow: 96 | 97 | ```js 98 | // @flow 99 | 100 | import Task from 'fun-task' 101 | 102 | function incrementTask(task: Task): Task { 103 | return task.map(x => x + 1) 104 | } 105 | ``` 106 | 107 | ## Specifications compatibility 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Task is compatible with [Fantasy Land](https://github.com/fantasyland/fantasy-land) and [Static Land](https://github.com/rpominov/static-land) implementing: 117 | 118 | - [Semigroup](https://github.com/fantasyland/fantasy-land#semigroup) 119 | - [Monoid](https://github.com/fantasyland/fantasy-land#monoid) 120 | - [Functor](https://github.com/fantasyland/fantasy-land#functor) 121 | - [Bifunctor](https://github.com/fantasyland/fantasy-land#bifunctor) 122 | - [Apply](https://github.com/fantasyland/fantasy-land#apply) 123 | - [Applicative](https://github.com/fantasyland/fantasy-land#applicative) 124 | - [Chain](https://github.com/fantasyland/fantasy-land#chain) 125 | - [Monad](https://github.com/fantasyland/fantasy-land#monad) 126 | 127 | ## Development 128 | 129 | ``` 130 | npm run lobot -- --help 131 | ``` 132 | 133 | Run [lobot](https://github.com/rpominov/lobot) commands as `npm run lobot -- args...` 134 | -------------------------------------------------------------------------------- /docs/api-reference.md: -------------------------------------------------------------------------------- 1 | # Task API Reference 2 | 3 | ## `Task.create(computation)` 4 | 5 | Creates a Task from a computation. Computation is a function that accepts two callbacks. 6 | It should call one of them after completion with the final result (success or failure). 7 | Also a computation may return a function with cancellation logic 8 | or it can return `undefined` if there is no cancellation logic. 9 | 10 | ```js 11 | const task = Task.create((onSuccess, onFailure) => { 12 | // ... 13 | return () => { 14 | // cancellation logic 15 | } 16 | }) 17 | 18 | // The computation is executed every time we run the task 19 | const cancel = task.run({ 20 | success(result) { 21 | // success result goes here 22 | }, 23 | failure(result) { 24 | // failure result goes here 25 | }, 26 | }) 27 | 28 | // If we cancel the task the cancellation logic from computation 29 | // will be executed (if provided) 30 | cancel() 31 | ``` 32 | 33 | Here is some runnable example: 34 | 35 | ```js 36 | const wait5sec = Task.create(onSuccess => { 37 | const timeoutId = setTimeout(() => { 38 | onSuccess('5 seconds') 39 | }, 5000) 40 | return () => { clearTimeout(timeoutId) } 41 | }) 42 | 43 | wait5sec.run({ 44 | success(timeWaited) { 45 | console.log(`We've waited for ${timeWaited}`) 46 | }, 47 | }) 48 | 49 | // > We've waited for 5 seconds 50 | ``` 51 | 52 | After cancellation or completion the `onSuccess` and `onFailure` callbacks become noop. 53 | Also if `cancel` called second time or after a completion the cancelation logic won't be executed. 54 | 55 | ## `Task.of(value)` 56 | 57 | Creates a task that resolves with a given value. 58 | 59 | ```js 60 | Task.of(2).run({ 61 | success(x) { 62 | console.log(`result: ${x}`) 63 | }, 64 | }) 65 | 66 | // > result: 2 67 | ``` 68 | 69 | ## `Task.rejected(error)` 70 | 71 | Creates a task that fails with a given error. 72 | 73 | ```js 74 | Task.rejected(2).run({ 75 | failure(error) { 76 | console.log(`error: ${error}`) 77 | }, 78 | }) 79 | 80 | // > error: 2 81 | ``` 82 | 83 | ## `Task.empty()` 84 | 85 | Creates a task that never completes. 86 | 87 | ```js 88 | Task.empty().run({ 89 | success(x) { 90 | // callback never called 91 | }, 92 | failure(error) { 93 | // callback never called 94 | }, 95 | }) 96 | ``` 97 | 98 | ## `task.map(fn)` 99 | 100 | > Static alias: `Task.map(fn, task)` 101 | 102 | Transforms a task by applying `fn` to the successful value. 103 | 104 | ```js 105 | Task.of(2).map(x => x * 3).run({ 106 | success(x) { 107 | console.log(`result: ${x}`) 108 | }, 109 | }) 110 | 111 | // > result: 6 112 | ``` 113 | 114 | ## `task.mapRejected(fn)` 115 | 116 | > Static alias: `Task.mapRejected(fn, task)` 117 | 118 | Transforms a task by applying `fn` to the failure value. 119 | 120 | ```js 121 | Task.rejected(2).mapRejected(x => x * 3).run({ 122 | failure(error) { 123 | console.log(`error: ${error}`) 124 | }, 125 | }) 126 | 127 | // > error: 6 128 | ``` 129 | 130 | ## `task.bimap(fFn, sFn)` 131 | 132 | > Static alias: `Task.bimap(fFn, sFn, task)` 133 | 134 | Transforms a task by applying `fFn` to the failure value or `sFn` to the successful value. 135 | 136 | ```js 137 | Task.of(2).bimap(x => x, x => x * 3).run({ 138 | success(x) { 139 | console.log(`result: ${x}`) 140 | }, 141 | }) 142 | 143 | // > result: 6 144 | ``` 145 | 146 | ```js 147 | Task.rejected(2).bimap(x => x * 3, x => x).run({ 148 | failure(error) { 149 | console.log(`error: ${error}`) 150 | }, 151 | }) 152 | 153 | // > error: 6 154 | ``` 155 | 156 | ## `task.chain(fn)` 157 | 158 | > Static alias: `Task.chain(fn, task)` 159 | 160 | Transforms a task by applying `fn` to the successful value, where `fn` returns a Task. 161 | 162 | ```js 163 | Task.of(2).chain(x => Task.of(x * 3)).run({ 164 | success(x) { 165 | console.log(`result: ${x}`) 166 | }, 167 | }) 168 | 169 | // > result: 6 170 | ``` 171 | 172 | The function can return a task that fails of course. 173 | 174 | ```js 175 | Task.of(2).chain(x => Task.rejected(x * 3)).run({ 176 | failure(error) { 177 | console.log(`error: ${error}`) 178 | }, 179 | }) 180 | 181 | // > error: 6 182 | ``` 183 | 184 | ## `task.orElse(fn)` 185 | 186 | > Static alias: `Task.orElse(fn, task)` 187 | 188 | Transforms a task by applying `fn` to the failure value, where `fn` returns a Task. 189 | Similar to `chain` but for failure path. 190 | 191 | ```js 192 | Task.rejected(2).orElse(x => Task.of(x * 3)).run({ 193 | success(x) { 194 | console.log(`result: ${x}`) 195 | }, 196 | }) 197 | 198 | // > result: 6 199 | ``` 200 | 201 | ## `tFn.ap(tX)` 202 | 203 | > Static alias: `Task.ap(tFn, tX)` 204 | 205 | Applies the successful value of task `tFn` to to the successful value of task `tX`. 206 | Uses `chain` under the hood, if you need parallel execution use `parallel`. 207 | 208 | ```js 209 | Task.of(x => x * 3).ap(Task.of(2)).run({ 210 | success(x) { 211 | console.log(`result: ${x}`) 212 | }, 213 | }) 214 | 215 | // > result: 6 216 | ``` 217 | 218 | 219 | ## `task.concat(otherTask)` 220 | 221 | > Static alias: `Task.concat(task, otherTask)` 222 | 223 | Selects the earlier of the two tasks. Uses `race` under the hood. 224 | 225 | ```js 226 | const task1 = Task.create(suc => { 227 | const id = setTimeout(() => suc(1), 1000) 228 | return () => { clearTimeout(id) } 229 | }) 230 | 231 | const task2 = Task.create(suc => { 232 | const id = setTimeout(() => suc(2), 2000) 233 | return () => { clearTimeout(id) } 234 | }) 235 | 236 | task1.concat(task2).run({ 237 | success(x) { 238 | console.log(`result: ${x}`) 239 | }, 240 | }) 241 | 242 | // > result: 1 243 | ``` 244 | 245 | 246 | 247 | ## `Task.parallel(tasks)` 248 | 249 | Given array of tasks creates a task of array. When result task executed given tasks will be executed in parallel. 250 | 251 | ```js 252 | Task.parallel([Task.of(2), Task.of(3)]).run( 253 | success(xs) { 254 | console.log(`result: ${xs.join(', ')}`) 255 | }, 256 | ) 257 | 258 | // > result: 2, 3 259 | ``` 260 | 261 | If any of given tasks fail, the result taks will also fail with the same error. 262 | In this case tasks that are still running are canceled. 263 | 264 | ```js 265 | Task.parallel([Task.of(2), Task.rejected(3)]).run( 266 | failure(error) { 267 | console.log(`error: ${error}`) 268 | }, 269 | ) 270 | 271 | // > error: 3 272 | ``` 273 | 274 | ## `Task.race(tasks)` 275 | 276 | Given array of tasks creates a task that completes with the earliest successful or failure value. 277 | After the fastest task completes other tasks are canceled. 278 | 279 | ```js 280 | const task1 = Task.create(suc => { 281 | const id = setTimeout(() => suc(1), 1000) 282 | return () => { 283 | console.log('canceled: 1') 284 | clearTimeout(id) 285 | } 286 | }) 287 | 288 | const task2 = Task.create(suc => { 289 | const id = setTimeout(() => suc(2), 2000) 290 | return () => { 291 | console.log('canceled: 2') 292 | clearTimeout(id) 293 | } 294 | }) 295 | 296 | Task.race([task1, task2]).run({ 297 | success(x) { 298 | console.log(`result: ${x}`) 299 | }, 300 | }) 301 | 302 | // > canceled: 2 303 | // > result: 1 304 | ``` 305 | 306 | ## `Task.do(generator)` 307 | 308 | This is something like [Haskell's do notation](https://en.wikibooks.org/wiki/Haskell/do_notation) 309 | or JavaScritp's async/await based on [generators](https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Iterators_and_Generators). 310 | 311 | You pass a generator that `yiels` and `returns` tasks and get a task in return. 312 | The whole proccess is pure, tasks are not being ran until the result task is ran. 313 | 314 | Here is a not runnable but somewhat real-world example: 315 | 316 | ```js 317 | // gets user from our API, returns a Task 318 | const getUserFromAPI = ... 319 | 320 | // gets zip code for given address using 3rd party API, returns a Task 321 | const getZipCode = ... 322 | 323 | function getUsersZip(userId) { 324 | return Task.do(function* () { 325 | const user = yield getUserFromAPI(userId) 326 | if (!user.address) { 327 | return Task.rejected({type: 'user_dont_have_address'}) 328 | } 329 | return getZipCode(user.address) 330 | }) 331 | } 332 | 333 | // Same function re-written using chain instead of do 334 | function getUsersZip(userId) { 335 | return getUserFromAPI(userId).chain(user => { 336 | if (!user.address) { 337 | return Task.rejected({type: 'user_dont_have_address'}) 338 | } 339 | return getZipCode(user.address) 340 | }) 341 | } 342 | 343 | getUsersZip(42).run({ 344 | success(zip) { 345 | // ... 346 | }, 347 | failure(error) { 348 | // The error here is either {type: 'user_dont_have_address'} 349 | // or some of errors that getUserFromAPI or getZipCode can produce 350 | // ... 351 | }, 352 | }) 353 | ``` 354 | 355 | And here's some runnable example: 356 | 357 | ```js 358 | Task.do(function* () { 359 | const a = yield Task.of(2) 360 | const b = yield Task.of(3) 361 | return Task.of(a * b) 362 | }).run({ 363 | success(x) { 364 | console.log(`result: ${x}`) 365 | }, 366 | }) 367 | 368 | // > result: 6 369 | ``` 370 | 371 | 372 | ## `task.run(handlers)` 373 | 374 | Runs the task. The `handlers` argument can contain 3 kinds of handlers `success`, `failure`, and `catch`. 375 | All handlers are optional, if you want to run task without handlers do it like this `task.run({})`. 376 | If a function passed as `handlers` it's automatically transformend to `{success: fn}`, 377 | so if you need only success handler you can do `task.run(x => ...)`. 378 | 379 | If `failure` handler isn't provided but task fails, an exception is thrown. 380 | You should always provided `failure` handlers for tasks that may fail. 381 | If you want to ignore failure pass a `noop` failure handler explicitly. 382 | 383 | The `catch` handler is for errors thrown from functions passed to `map`, `chain` etc. 384 | [More on how it works](./exceptions.md#how-exceptions-work-in-task). 385 | 386 | ```js 387 | Task.of(2).run({ 388 | success(x) { 389 | console.log(`result: ${x}`) 390 | }, 391 | failure(error) { 392 | // handle failure ... 393 | }, 394 | catch(error) { 395 | // handle error thrown from `map(fn)` etc ... 396 | }, 397 | }) 398 | 399 | // > result: 2 400 | ``` 401 | 402 | ## `task.runAndLog()` 403 | 404 | Runs the task and prints results using `console.log()`. Mainly for testing / debugging etc. 405 | 406 | ```js 407 | Task.of(2).runAndLog() 408 | 409 | // > Success: 2 410 | ``` 411 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {runGenerator} from 'static-land' 4 | 5 | type Cancel = () => void 6 | type Handler<-T> = (x: T) => void 7 | type Handlers<-S, -F> = { 8 | success: Handler, 9 | failure: Handler, 10 | catch?: Handler, 11 | } 12 | type LooseHandlers<-S, -F> = Handler | { 13 | success?: Handler, 14 | failure?: Handler, 15 | catch?: Handler, 16 | } 17 | type Computation<+S, +F> = (handleSucc: Handler, handleFail: Handler) => ?Cancel 18 | 19 | const defaultFailureHandler: Handler = failure => { 20 | if (failure instanceof Error) { 21 | throw failure 22 | } else { 23 | throw new Error(`Unhandled task failure: ${String(failure)}`) 24 | } 25 | } 26 | const noop = () => {} 27 | 28 | type RunHelperBody = (s: Handler, f: Handler, c?: Handler) => { 29 | onCancel?: Cancel, // called only when user cancels 30 | onClose?: Cancel, // called when user cancels plus when succ/fail/catch are called 31 | } 32 | const runHelper = (body: RunHelperBody, handlers: Handlers): Cancel => { 33 | let {success, failure, catch: catch_} = handlers 34 | let onCancel = noop 35 | let onClose = noop 36 | let close = () => { 37 | onClose() 38 | // The idea here is to kill links to all stuff that we exposed from runHelper closure. 39 | // We expose via the return value (cancelation function) and by passing callbacks to the body. 40 | // We reason from an assumption that outer code may keep links to values that we exposed forever. 41 | // So we look at all things that referenced in the exposed callbacks and kill them. 42 | success = noop 43 | failure = noop 44 | catch_ = noop 45 | onCancel = noop 46 | close = noop 47 | } 48 | const bodyReturn = body( 49 | x => { 50 | const s = success 51 | close() 52 | s(x) 53 | }, 54 | x => { 55 | const f = failure 56 | close() 57 | f(x) 58 | }, 59 | catch_ && (x => { 60 | const c = (catch_: any) 61 | close() 62 | c(x) 63 | }) 64 | ) 65 | onCancel = bodyReturn.onCancel || noop 66 | onClose = bodyReturn.onClose || noop 67 | if (close === noop) { 68 | onCancel = noop 69 | onClose() 70 | } 71 | return () => { onCancel(); close() } 72 | } 73 | 74 | 75 | 76 | export default class Task<+S, +F> { 77 | 78 | constructor() { 79 | if (this.constructor === Task) { 80 | throw new Error('Don\'t call `new Task()`, call `Task.create()` instead') 81 | } 82 | } 83 | 84 | // Creates a task with an arbitrary computation 85 | static create(computation: Computation): Task { 86 | return new FromComputation(computation) 87 | } 88 | 89 | // Creates a task that resolves with a given value 90 | static of(value: S): Task { 91 | return new Of(value) 92 | } 93 | // instance alias for Fantasy Land 94 | of(value: S): Task { 95 | return Task.of(value) 96 | } 97 | 98 | // Creates a task that fails with a given error 99 | static rejected(error: F): Task { 100 | return new Rejected(error) 101 | } 102 | 103 | // Creates a task that never completes 104 | static empty(): Task { 105 | return new Empty() 106 | } 107 | // instance alias for Fantasy Land 108 | empty(): Task { 109 | return Task.empty() 110 | } 111 | 112 | // Given array of tasks creates a task of array 113 | static parallel(tasks: Array>): Task { 114 | return new Parallel(tasks) 115 | } 116 | 117 | // Given array of tasks creates a task that completes with the earliest value or error 118 | static race(task: Array>): Task { 119 | return new Race(task) 120 | } 121 | 122 | // Transforms a task by applying `fn` to the successful value 123 | static map(fn: (x: S) => S1, task: Task): Task { 124 | return new Map(task, fn) 125 | } 126 | map(fn: (x: S) => S1): Task { 127 | return new Map(this, fn) 128 | } 129 | 130 | // Transforms a task by applying `fn` to the failure value 131 | static mapRejected(fn: (x: F) => F1, task: Task): Task { 132 | return new MapRejected(task, fn) 133 | } 134 | mapRejected(fn: (x: F) => F1): Task { 135 | return new MapRejected(this, fn) 136 | } 137 | 138 | // Transforms a task by applying `sf` to the successful value or `ff` to the failure value 139 | static bimap(ff: (x: F) => F1, fs: (x: S) => S1, task: Task): Task { 140 | return task.map(fs).mapRejected(ff) 141 | } 142 | bimap(ff: (x: F) => F1, fs: (x: S) => S1): Task { 143 | return this.map(fs).mapRejected(ff) 144 | } 145 | 146 | // Transforms a task by applying `fn` to the successful value, where `fn` returns a Task 147 | static chain(fn: (x: S) => Task, task: Task): Task { 148 | return new Chain(task, fn) 149 | } 150 | chain(fn: (x: S) => Task): Task { 151 | return new Chain(this, fn) 152 | } 153 | 154 | // Transforms a task by applying `fn` to the failure value, where `fn` returns a Task 155 | static orElse(fn: (x: F) => Task, task: Task): Task { 156 | return new OrElse(task, fn) 157 | } 158 | orElse(fn: (x: F) => Task): Task { 159 | return new OrElse(this, fn) 160 | } 161 | 162 | // Applies the successful value of task `this` to to the successful value of task `otherTask` 163 | static ap(tf: Task<(x: A) => B, F1>, tx: Task): Task { 164 | return tf.chain(f => tx.map(x => f(x))) 165 | } 166 | ap(otherTask: Task): Task { 167 | return this.chain(f => otherTask.map(x => (f: any)(x))) 168 | } 169 | 170 | // Selects the earlier of the two tasks 171 | static concat(a: Task, b: Task): Task { 172 | return Task.race([a, b]) 173 | } 174 | concat(otherTask: Task): Task { 175 | return Task.race([this, otherTask]) 176 | } 177 | 178 | static do(generator: () => Generator, Task, mixed>): Task { 179 | return runGenerator(Task, generator) 180 | } 181 | 182 | _run(handlers: Handlers): Cancel { // eslint-disable-line 183 | throw new Error('Method run() is not implemented in basic Task class.') 184 | } 185 | 186 | _toString(): string { 187 | return '' 188 | } 189 | 190 | toString() { 191 | return `Task.${this._toString()}` 192 | } 193 | 194 | run(h: LooseHandlers): Cancel { 195 | const handlers = typeof h === 'function' 196 | ? {success: h, failure: defaultFailureHandler} 197 | : {success: h.success || noop, failure: h.failure || defaultFailureHandler, catch: h.catch} 198 | return this._run(handlers) 199 | } 200 | 201 | runAndLog(): void { 202 | this.run({ 203 | success(x) { console.log('Success:', x) }, // eslint-disable-line 204 | failure(x) { console.log('Failure:', x) }, // eslint-disable-line 205 | }) 206 | } 207 | 208 | } 209 | 210 | class FromComputation extends Task { 211 | 212 | _computation: Computation; 213 | 214 | constructor(computation: Computation) { 215 | super() 216 | this._computation = computation 217 | } 218 | 219 | _run(handlers: Handlers) { 220 | const {_computation} = this 221 | return runHelper((s, f, c) => { 222 | let cancel 223 | if (c) { 224 | try { 225 | cancel = _computation(s, f) 226 | } catch (e) { c(e) } 227 | } else { 228 | cancel = _computation(s, f) 229 | } 230 | return {onCancel: cancel || noop} 231 | }, handlers) 232 | } 233 | 234 | _toString() { 235 | return 'create(..)' 236 | } 237 | 238 | } 239 | 240 | class Of extends Task { 241 | 242 | _value: S; 243 | 244 | constructor(value: S) { 245 | super() 246 | this._value = value 247 | } 248 | 249 | _run(handlers: Handlers): Cancel { 250 | const {success} = handlers 251 | success(this._value) 252 | return noop 253 | } 254 | 255 | _toString() { 256 | return `of(${String(this._value)})` 257 | } 258 | 259 | } 260 | 261 | class Rejected extends Task { 262 | 263 | _error: F; 264 | 265 | constructor(error: F) { 266 | super() 267 | this._error = error 268 | } 269 | 270 | _run(handlers: Handlers): Cancel { 271 | const {failure} = handlers 272 | failure(this._error) 273 | return noop 274 | } 275 | 276 | _toString() { 277 | return `rejected(${String(this._error)})` 278 | } 279 | 280 | } 281 | 282 | class Empty extends Task { 283 | 284 | run(): Cancel { 285 | return noop 286 | } 287 | 288 | _toString() { 289 | return `empty()` 290 | } 291 | 292 | } 293 | 294 | class Parallel extends Task { 295 | 296 | _tasks: Array>; 297 | 298 | constructor(tasks: Array>) { 299 | super() 300 | this._tasks = tasks 301 | } 302 | 303 | _run(handlers: Handlers): Cancel { 304 | return runHelper((s, f, c) => { 305 | const length = this._tasks.length 306 | const values: Array = Array(length) 307 | let completedCount = 0 308 | const runTask = (task, index) => task.run({ 309 | success(x) { 310 | values[index] = x 311 | completedCount++ 312 | if (completedCount === length) { 313 | s((values: any)) 314 | } 315 | }, 316 | failure: f, 317 | catch: c, 318 | }) 319 | const cancels = this._tasks.map(runTask) 320 | return {onClose() { cancels.forEach(cancel => cancel()) }} 321 | }, handlers) 322 | } 323 | 324 | _toString() { 325 | return `parallel([${this._tasks.map(x => x._toString()).join(', ')}])` 326 | } 327 | 328 | } 329 | 330 | class Race extends Task { 331 | 332 | _tasks: Array>; 333 | 334 | constructor(tasks: Array>) { 335 | super() 336 | this._tasks = tasks 337 | } 338 | 339 | _run(handlers: Handlers): Cancel { 340 | return runHelper((success, failure, _catch) => { 341 | const handlers = {success, failure, catch: _catch} 342 | const cancels = this._tasks.map(task => task.run(handlers)) 343 | return {onClose() { cancels.forEach(cancel => cancel()) }} 344 | }, handlers) 345 | } 346 | 347 | _toString() { 348 | return `race([${this._tasks.map(x => x._toString()).join(', ')}])` 349 | } 350 | 351 | } 352 | 353 | class Map extends Task { 354 | 355 | _task: Task; 356 | _fn: (x: SIn) => SOut; 357 | 358 | constructor(task: Task, fn: (x: SIn) => SOut) { 359 | super() 360 | this._task = task 361 | this._fn = fn 362 | } 363 | 364 | _run(handlers: Handlers): Cancel { 365 | const {_fn} = this 366 | const {success, failure, catch: catch_} = handlers 367 | return this._task.run({ 368 | success(x) { 369 | let value 370 | if (catch_) { 371 | try { 372 | value = _fn(x) 373 | } catch (e) { 374 | catch_(e) 375 | return 376 | } 377 | } else { 378 | value = _fn(x) 379 | } 380 | success(value) 381 | }, 382 | failure, 383 | catch: catch_, 384 | }) 385 | } 386 | 387 | _toString() { 388 | return `${this._task._toString()}.map(..)` 389 | } 390 | } 391 | 392 | class MapRejected extends Task { 393 | 394 | _task: Task; 395 | _fn: (x: FIn) => FOut; 396 | 397 | constructor(task: Task, fn: (x: FIn) => FOut) { 398 | super() 399 | this._task = task 400 | this._fn = fn 401 | } 402 | 403 | _run(handlers: Handlers): Cancel { 404 | const {_fn} = this 405 | const {success, failure, catch: catch_} = handlers 406 | return this._task.run({ 407 | success, 408 | failure(x) { 409 | let value 410 | if (catch_) { 411 | try { 412 | value = _fn(x) 413 | } catch (e) { 414 | catch_(e) 415 | return 416 | } 417 | } else { 418 | value = _fn(x) 419 | } 420 | failure(value) 421 | }, 422 | catch: catch_, 423 | }) 424 | } 425 | 426 | _toString() { 427 | return `${this._task._toString()}.mapRejected(..)` 428 | } 429 | } 430 | 431 | class Chain extends Task { 432 | 433 | _task: Task; 434 | _fn: (x: SIn) => Task; 435 | 436 | constructor(task: Task, fn: (x: SIn) => Task) { 437 | super() 438 | this._task = task 439 | this._fn = fn 440 | } 441 | 442 | _run(handlers: Handlers): Cancel { 443 | const {_fn} = this 444 | return runHelper((success, failure, catch_) => { 445 | let cancel2 = noop 446 | const cancel1 = this._task.run({ 447 | success(x) { 448 | let spawned 449 | if (catch_) { 450 | try { 451 | spawned = _fn(x) 452 | } catch (e) { catch_(e) } 453 | } else { 454 | spawned = _fn(x) 455 | } 456 | if (spawned) { 457 | cancel2 = spawned.run({success, failure, catch: catch_}) 458 | } 459 | }, 460 | failure, 461 | catch: catch_, 462 | }) 463 | return {onCancel() { cancel1(); cancel2() }} 464 | }, handlers) 465 | } 466 | 467 | _toString() { 468 | return `${this._task._toString()}.chain(..)` 469 | } 470 | } 471 | 472 | class OrElse extends Task { 473 | 474 | _task: Task; 475 | _fn: (x: FIn) => Task; 476 | 477 | constructor(task: Task, fn: (x: FIn) => Task) { 478 | super() 479 | this._task = task 480 | this._fn = fn 481 | } 482 | 483 | _run(handlers: Handlers): Cancel { 484 | const {_fn} = this 485 | return runHelper((success, failure, catch_) => { 486 | let cancel2 = noop 487 | const cancel1 = this._task.run({ 488 | success, 489 | failure(x) { 490 | let spawned 491 | if (catch_) { 492 | try { 493 | spawned = _fn(x) 494 | } catch (e) { catch_(e) } 495 | } else { 496 | spawned = _fn(x) 497 | } 498 | if (spawned) { 499 | cancel2 = spawned.run({success, failure, catch: catch_}) 500 | } 501 | }, 502 | catch: catch_, 503 | }) 504 | return {onCancel() { cancel1(); cancel2() }} 505 | }, handlers) 506 | 507 | } 508 | 509 | _toString() { 510 | return `${this._task._toString()}.orElse(..)` 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /docs/exceptions.md: -------------------------------------------------------------------------------- 1 | # Try..catch in JavaScript async abstractions like Promise or Task 2 | 3 | This article explains the reasoning behind how errors catching works in Task. 4 | It starts from very fundamental concepts, but it's necessary to avoid misunderstanding 5 | later in the article when some terms from earlier parts are used. 6 | The closer to the end the more practical matters are discussed. 7 | 8 | ## Expected and unexpected code paths 9 | 10 | In any program (especially in JavaScript) there always expected and unexpected code paths. 11 | When a program goes through an unexpected path we call it "a bug". And when it goes only through expected paths 12 | is just normal execution of the program. Consider this example: 13 | 14 | ```js 15 | let x = Math.random() - 0.5 16 | let y 17 | let z 18 | 19 | if (x > 0) { 20 | y = 10 21 | } else { 22 | z = 10 23 | } 24 | 25 | if (x <= 0) { 26 | alert(z + 5) 27 | } else { 28 | alert(y + 5) 29 | } 30 | ``` 31 | 32 | Here are two expected paths of this program: 33 | 34 | 35 | 36 | And here is an unexpected path: 37 | 38 | 39 | 40 | We as programmers don't expect this to ever happen with this program. 41 | But if, for example, we change first condition and forget to change second one program may 42 | run through the unexpected path. That would be a bug. 43 | 44 | 45 | ## Railway oriented programming / split expected path in two 46 | 47 | Also we can introduce some abstractions and semantics that would split **expected path** into 48 | **expected success** and **expected failure**. If you understand `Either` type you know what I'm talking about. 49 | This is fairly common pattern in FP world, I'll try to explain it briefly, but here are some good 50 | articles that do a much better job: 51 | 52 | - ["Railway oriented programming"](https://fsharpforfunandprofit.com/posts/recipe-part2/) 53 | - ["A Monad in Practicality: First-Class Failures"](http://robotlolita.me/2013/12/08/a-monad-in-practicality-first-class-failures.html) 54 | - ["Practical Intro to Monads in JavaScript: Either"](https://tech.evojam.com/2016/03/21/practical-intro-to-monads-in-javascript-either/) 55 | 56 | Say we build a simple CLI program that takes a number `n` from user and prints `1/n`. 57 | 58 | ```js 59 | function print(str) { 60 | console.log(str) 61 | } 62 | 63 | function main(userInput) { 64 | const number = parseInt(userInput, 10) 65 | 66 | if (Number.isNaN(number)) { 67 | print(`Not a number: ${userInput}`) 68 | } else { 69 | if (number === 0) { 70 | print(`Cannot divide by zero`) 71 | } else { 72 | print(1 / number) 73 | } 74 | } 75 | 76 | } 77 | 78 | // Read a line from stdin somehow and apply main() to it. 79 | // Details of how it's done are not important for this example. 80 | main(inputFromStdin) 81 | ``` 82 | 83 | In this example execution flow of the program looks like this: 84 | 85 | 86 | 87 | As you can see program splits in two places. This happens very often in programs. 88 | In some cases all branches look neutral, in other cases (like this) we can consider one path as 89 | a success and another one as a failure. We can make the distinguish more formal by 90 | introducing an abstraction: 91 | 92 | ```js 93 | const Either = { 94 | chain(fn, either) { 95 | return ('success' in either) ? fn(either.success) : either 96 | }, 97 | fork(onSuccess, onFailure, either) { 98 | return ('success' in either) ? onSuccess(either.success) : onFailure(either.failure) 99 | }, 100 | } 101 | ``` 102 | 103 | Now we can rewrite our example using `Either`: 104 | 105 | ```js 106 | function print(str) { 107 | console.log(str) 108 | } 109 | 110 | function parse(str) { 111 | const number = parseInt(str, 10) 112 | return Number.isNaN(number) ? {failure: `Not a number: ${str}`} : {success: number} 113 | } 114 | 115 | function calc(number) { 116 | return number === 0 ? {failure: `Cannot divide by zero`} : {success: 1 / number} 117 | } 118 | 119 | function main(userInput) { 120 | const parsed = parse(userInput) 121 | const calculated = Either.chain(calc, parsed) 122 | Either.fork(print, print, calculated) 123 | } 124 | ``` 125 | 126 | In this version flow looks more like the folowing. It looks simpler, like we simply write code 127 | that cannot fail and Either takes care of managing failure branch. 128 | 129 | 130 | 131 | Maybe this doesn't make much sense to you now (if you're not familiar with Either). 132 | And this is by no means a complete explanation of Either pattern (check out resources 133 | I've mentioned above for better explanations). But for the purpose of this article the only 134 | thing we need to take out of this section is that some paths in program can be 135 | treated formally or informally as **expected failures**. 136 | 137 | Let's recap. We've split all possible paths in programs to three groups: 138 | 139 | - **Expected success** is the main happy path of the program, 140 | it represents how program behaves when everything goes right. 141 | - **Expected failure** is secondary path that represent 142 | all expected deviations from happy path e.g., when user gives an incorrect input. 143 | - **Unexpected failure** is some *unexpected* deviations from main or secondary paths, 144 | something that we call "bugs". 145 | 146 | 147 | ## try..catch 148 | 149 | How does `try..catch` fits into our three code paths groups view? It's great for unexpected failures! 150 | Or we should say: `throw` great for unexpected failures if we never actually `try..catch`. 151 | It's very good for debugging. Debugger will pause on the exact line that throws. 152 | Also if we don't use debugger we still get nice stack trace in console etc. It's sad that in many 153 | cases when program goes through unexpected path instead of exception we end up with `NaN` 154 | being propagated through program or something like that. In these cases it's much harder to track 155 | down where things went wrong, much nicer when it just throws. 156 | 157 | On the other hand `try..catch` is bad for expected failures. There're many reasons why, but let's 158 | focus on just one: *it's bad for expected failures because it's already used for unexpected ones.* 159 | We must handle expected failures, so we would need to `try..catch` function that uses 160 | `throw` for expected failure. But if we do that we'll catch not only errors that represent 161 | expected failures, but also random errors that represent bugs. This is bad for two reasons: 162 | 163 | 1. we ruin nice debugging experience (debugger will no longer pause etc); 164 | 2. in our code that is supposed to handle expected failures we would need to also 165 | handle unexpected failures (which is generally imposible as shown in the next section). 166 | 167 | If throw is used for expected failures in some API, we should wrap into `try..catch` as little code as 168 | possible, so we won't also catch bugs by accident. 169 | 170 | 171 | ## How program should behave in case of unexpected failures 172 | 173 | Try..catch provide us with a mechanism for writing code that will be executed in case of *some* 174 | unexpected failures. We can just wrap arbitrary code into `try..catch`, and we catch bugs 175 | that express themselves as exceptions in that code. Should we use this mechanism and what 176 | handling code in `catch(e) {..}` should do? 177 | 178 | Let's look at this from theoretical point of view first and dive into practical 179 | details in next sections. 180 | 181 | First of all let's focus on the fact that this mechanism catches only **some** failures. 182 | In many cases program may not throw but just behave incorrectly in some way. 183 | In my expirience with JavaScript I'd estimate that it throws only in about 30% of cases. 184 | So should we even care to use this mechanism if it works only in 30% cases? 185 | 186 | If we still want to use it, what the handling code should do? I can think of two options: 187 | 188 | 1. Try to completelly recover somehow and keep program running. 189 | 2. Crash / restart program and log / report about the bug. 190 | 191 | The `#1` option is simply impossible. We can't transition program from arbitrary 192 | unexpected (inconsistent) state to an expected (consistent) state. For the simple reason that 193 | starting state is **unexpected** — we don't know anything about it, because we didn't expect it. 194 | How could we transition from a state of which we don't know anything to any other state? 195 | There is one way to do it though — restart the program, which is our `#2` option. 196 | 197 | Also any code that is executed in responce to a bug have a potential to make things worse. 198 | It transitions program to even more complicated inconsistent state. Plus if program continue to run 199 | the inconsistent state may leak to database. In this scenario even a restart may not help. 200 | And if many users are connected to a single database they all may start to experience the bug. 201 | 202 | The `#2` is often happens automatically (at least crash part), so maybe we don't 203 | even need to `catch`. But it's ok to cathc for `#2` purposes. 204 | 205 | 206 | ## Unexpected failures in Node 207 | 208 | We could restart the server on each unhandled exception, but this is problematic because server 209 | usually handles several requests concurently at the same time. So if we restart the server 210 | not only request that have faced a bug will fail, but all other requests that happen to be 211 | handled at the same time will fail as well. Some people think that a better approach is to wrap all 212 | the code that responsible for handling each request to some sort of `try..catch` block and when 213 | a error happens fail only one request. Although we can't use `try..catch` of course because the 214 | code is asynchronous. So we should use some async abstraction that can provide this functionality (e.g. Promises). 215 | 216 | Another option for Node is to let server crash. Yes, this will result in forcefully ending the execution of all other connections, resulting in more than a single user getting an error. But we will benefit from the crash by taking core dumps (`node --abort_on_uncaught_exception`) etc. 217 | 218 | Also in Node we can use the `uncaughtException` event combined with a tool like [naught](https://github.com/andrewrk/naught). Here is a qoute from naught docs: 219 | 220 | > Using naught a worker can use the 'offline' message to announce that it is dying. At this point, naught prevents it from accepting new connections and spawns a replacement worker, allowing the dying worker to finish up with its current connections and do any cleanup necessary before finally perishing. 221 | 222 | Conclusion: we might want to catch unexpected errors in Node, but there are plenty other options. 223 | 224 | 225 | ## Unexpected failures in browser 226 | 227 | In case of a browser restarting (reloading the page) usually considered as an awful behavior from 228 | the UX point of view, so it might be not an option. We may choose not to restart in a hope of 229 | providing a better UX at a risk of leaking inconsistent state to the database etc. Some bugs are 230 | indeed not fatal for a web page, and it often may continue to work mostly fine. So this is a 231 | trade–off and to not restart is a legitimate option here. 232 | 233 | Also in case of a browser we might want UI to react to the bug somehow. But in case of arbitrary 234 | bug there isn't much we can do again. In case of an *expected* failure (like the incorrect user input) 235 | we can handle it very well from UI/UX poit of view — we should show an error message near the exact 236 | field in the form, also we may dissable the submit button etc. In case of a bug we don't really know 237 | what is going on, so we can only do something like showing a popup with a very vague message. 238 | But I think this won't be very helpfull, it may actually be worse than not showing a popup. 239 | Maybe user not even going to interact with the part of the program that has broken, and a popup out 240 | of nowhere may only damage UX. And if user do interact with the broken part they will notice that 241 | it's broken anyway — no need to tell what they already know. Also if we show a popup user might 242 | assume that something failed, but now it's all under control and it's safe to continue to use the 243 | program. But this would be a lie, nothing is under control in case of a bug. 244 | 245 | Conclusion: we have no reason to catch unexpected errors in browser. 246 | 247 | 248 | ## Promises and expected failures 249 | 250 | Promises support two code paths. There're two callbacks in `then` etc. 251 | Also Promises automatically cathc all exceptions thrown from then's callbacks and put them into 252 | the next failure callback down the chain. 253 | 254 | So the second path is already used for unexpected failures. 255 | That makes it unusable for expected failures (see ["try..catch" section](#trycatch)). 256 | In other words Promises don't support Railways / Either pattern. If you want to use that pattern with Promises 257 | you should wrap Either into Promise. To use Promise's second path for this is a terrible idea. 258 | 259 | 260 | ## Should async abstractions support exceptions catching? 261 | 262 | From previous sections we've learned that we definitely may want to not catch exceptions at all. 263 | In this case we get the best debugging experience. Even if abstraction will cathc exceptions and then 264 | re-throw, it won't be the same as to not catch at all, for instance debugger won't pause on 265 | the original line of `throw`. 266 | 267 | But we also may want to catch "async exceptions", for instance in Node web server case. 268 | A perfect solution would be optional catching. 269 | 270 | Not all abstractions can support optional cathcing. If we have to choose between non-optional 271 | catching and not supporting catching at all we should choose latter. 272 | Non-optional catching hurts more than helps. 273 | 274 | This part seems to be ok in Promises. If we don't provide failure callback in `then` and don't use 275 | `catch` method it seems that debugger behaves the same way as if error wasn't catched 276 | (at least in current Chrome). Although it wasn't always this way, previously they used to simply 277 | swallow exceptions if there wasn't a catch callback. 278 | 279 | 280 | ## How exceptions work in Task 281 | 282 | In Task we want to support both **optional** errors catching and Railways / Either patern. 283 | When we `run()` a task we can choose whether errors will be catched or not, 284 | and if they are catched they go into a separate callback. 285 | 286 | ```js 287 | // exceptions are not catched 288 | task.run({ 289 | success(x) { 290 | // handle success 291 | }, 292 | failure(x) { 293 | // handle expected failure 294 | }, 295 | }) 296 | 297 | // if we provide catch callback exceptions are catched 298 | task.run({ 299 | success(x) { 300 | // handle success 301 | }, 302 | failure(x) { 303 | // handle expected failure 304 | }, 305 | catch(e) { 306 | // handle a bug 307 | }, 308 | }) 309 | ``` 310 | 311 | So if `catch` callback isn't provided, we can enjoy great debugging expirience in a browser (even if we have `failure` callback). And in Node we can still catch exceptions in async code if we want to. Also notice that we use a separate callback for exceptions, so we won't have to write code that have to handle both expected and unexpected failures. 312 | 313 | The default behaviour is to not catch. This is what we want in browser, and what also may be a legitimate option for Node. 314 | 315 | In Task the `catch` callback is reserved only for bug-exceptions. Expected exception must be wrappend in a `try..catch` block manually. All the API and semantics in Task are designed with this assumption in mind. 316 | 317 | Exceptions thrown from `success` and `failure` callbacks are never catched, even if `catch` callbacks is provided. 318 | 319 | ```js 320 | task.run({ 321 | success() { 322 | // this error won't be catched 323 | throw new Error('') 324 | }, 325 | catch(error) { 326 | // the error above will not go here 327 | } 328 | }) 329 | ``` 330 | 331 | This is done because otherwise we might end up with half of the code for `success` being executed plus the code for `catch`, which in most cases isn't what we want. For example in a web server case, we could start sending response for `success` case, but then continue by sending the response for `catch`. Instead we should catch manually: 332 | 333 | ```js 334 | task.run({ 335 | success() { 336 | try { 337 | // ... 338 | res.send(/* some part of success response */) 339 | // ... 340 | // supposedly some code here have thrown 341 | // ... 342 | } catch (e) { 343 | // do something about the exception 344 | // but keep in mind that "some part of success response" was already sent 345 | } 346 | }, 347 | catch(error) { 348 | // handle error thrown from .map(fn) etc. 349 | } 350 | }) 351 | ``` 352 | --------------------------------------------------------------------------------