Repository: char0n/ramda-adjunct (js)

Issue:
catchP
**Is your feature request related to a problem? Please describe.**

Functional alternative for `Promise.catch`

**Describe the solution you'd like**
```js
const promise = Promise.reject(3);
RA.catchP(console.dir, promise); //=> outputs 3
```
Possible implementation
```js
const catchP = R.invoker(1, 'catch');
```

**Describe alternatives you've considered**
--

**Additional context**
[Promise.prototype.catch](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch)


Pull request:
feat(function): add catchP
Closes #1232

Patch:
diff --git a/src/catchP.js b/src/catchP.js
new file mode 100644
index 0000000000..08f09c1a44
--- /dev/null
+++ b/src/catchP.js
@@ -0,0 +1,25 @@
+import { invoker } from 'ramda';
+
+/**
+ * Composable shortcut for `Promise.catch`.
+ * The catchP function returns a Promise. It takes two arguments: a callback function for the failure of the Promise
+ * and the promise instance itself.
+ *
+ * @func catchP
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.29.0|v2.29.0}
+ * @category Function
+ * @sig (a -> Promise b | b) -> Promise b
+ * @param {Function} onRejected A Function called if the Promise is rejected. This function has one argument, the rejection reason.
+ * @param {Promise} promise Any Promise
+ * @return {Promise} Returns a Promise with dealt rejected cases
+ * @see {@link RA.thenP|thenP}, {@link RA.resolveP|resolveP}, {@link RA.rejectP|rejectP}, {@link RA.allP|allP}
+ *
+ * @example
+ *
+ * RA.catchP(() => 'b', Promise.resolve('a')); //=> Promise('a')
+ * RA.catchP(() => 'b', Promise.reject('a')); //=> Promise('b')
+ */
+const catchP = invoker(1, 'catch');
+
+export default catchP;
diff --git a/src/index.js b/src/index.js
index 1898ecbb09..603944f724 100644
--- a/src/index.js
+++ b/src/index.js
@@ -108,6 +108,7 @@ export { default as weaveLazy } from './weaveLazy';
 export { default as curryRightN } from './curryRightN';
 export { default as curryRight } from './curryRight';
 export { default as allP } from './allP';
+export { default as catchP } from './catchP';
 export { default as noneP } from './noneP';
 export { default as resolveP } from './resolveP';
 export { default as rejectP } from './rejectP';
diff --git a/types/index.d.ts b/types/index.d.ts
index e80ac31261..887853ce30 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -1002,6 +1002,14 @@ declare namespace RamdaAdjunct {
             reject<T>(options: { timeout: number, value: T }): Promise<T>
         };
 
+        /**
+         * Composable shortcut for `Promise.catch`.
+         * The catchP function returns a Promise. It takes two arguments: a callback function for the rejections of the Promise
+         * and the promise instance itself.
+         */
+        catchP<A, B = unknown>(onRejected: (error: any) => B | Promise<B>, promise: Promise<A>): Promise<A | B>;
+        catchP<A, B = unknown>(onRejected: (error: any) => B | Promise<B>): (promise: Promise<A>) => Promise<A | B>;
+
         /**
          * Composable shortcut for `Promise.then`.
          * The thenP function returns a Promise. It takes two arguments: a callback function for the success of the Promise


Test patch:
diff --git a/test/catchP.js b/test/catchP.js
new file mode 100644
index 0000000000..b705ab1495
--- /dev/null
+++ b/test/catchP.js
@@ -0,0 +1,37 @@
+import { assert } from 'chai';
+import * as R from 'ramda';
+
+import * as RA from '../src';
+
+describe('catchP', function () {
+  context('given applied on Promise', function () {
+    specify(
+      'should call catch method with onRejected function',
+      async function () {
+        const promise = RA.rejectP(1);
+        const expected = await RA.catchP(R.add(1), promise);
+
+        assert.strictEqual(expected, 2);
+      }
+    );
+  });
+
+  context('given applied on Thenable', function () {
+    specify('should throw TypeError', function () {
+      const thenable = { then: (fn) => RA.rejectP(fn(1)) };
+
+      assert.throws(() => RA.catchP(R.identity, thenable), TypeError);
+    });
+  });
+
+  context('given applied on non-thenable', function () {
+    specify('should throw TypeError', function () {
+      assert.throws(() => RA.catchP(R.identity, {}), TypeError);
+    });
+  });
+
+  it('should be curried', async function () {
+    assert.strictEqual(await RA.catchP(R.identity, RA.rejectP(1)), 1);
+    assert.strictEqual(await RA.catchP(R.identity)(RA.rejectP(1)), 1);
+  });
+});
diff --git a/types/test/catchP.ts b/types/test/catchP.ts
new file mode 100644
index 0000000000..86a1758cb5
--- /dev/null
+++ b/types/test/catchP.ts
@@ -0,0 +1,9 @@
+import * as RA from 'ramda-adjunct';
+
+const always = <A>(x: A) => () => x;
+
+const promise = RA.rejectP(1);
+
+RA.catchP(always('a'), promise); // $ExpectType Promise<string | number>
+RA.catchP<number, string>(always('a'))(promise); // $ExpectType Promise<string | number>
+RA.catchP<number, string>(always('a'))(promise); // $ExpectType Promise<string | number>