Skip to content

Commit 4664a28

Browse files
authored
Merge pull request #615 from konker/add-or-tee
Add orTee method to tee the error track
2 parents 493e9e2 + 85ed7fd commit 4664a28

File tree

5 files changed

+192
-0
lines changed

5 files changed

+192
-0
lines changed

.changeset/tender-impalas-refuse.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'neverthrow': minor
3+
---
4+
5+
Add orTee, which is the equivalent of andTee but for the error track.

README.md

+96
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
3636
- [`Result.match` (method)](#resultmatch-method)
3737
- [`Result.asyncMap` (method)](#resultasyncmap-method)
3838
- [`Result.andTee` (method)](#resultandtee-method)
39+
- [`Result.orTee` (method)](#resultortee-method)
3940
- [`Result.andThrough` (method)](#resultandthrough-method)
4041
- [`Result.asyncAndThrough` (method)](#resultasyncandthrough-method)
4142
- [`Result.fromThrowable` (static class method)](#resultfromthrowable-static-class-method)
@@ -55,6 +56,7 @@ For asynchronous tasks, `neverthrow` offers a `ResultAsync` class which wraps a
5556
- [`ResultAsync.orElse` (method)](#resultasyncorelse-method)
5657
- [`ResultAsync.match` (method)](#resultasyncmatch-method)
5758
- [`ResultAsync.andTee` (method)](#resultasyncandtee-method)
59+
- [`ResultAsync.orTee` (method)](#resultasyncortee-method)
5860
- [`ResultAsync.andThrough` (method)](#resultasyncandthrough-method)
5961
- [`ResultAsync.combine` (static class method)](#resultasynccombine-static-class-method)
6062
- [`ResultAsync.combineWithAllErrors` (static class method)](#resultasynccombinewithallerrors-static-class-method)
@@ -593,6 +595,53 @@ resAsync.then((res: Result<void, ParseError | InsertError>) => {e
593595

594596
---
595597

598+
#### `Result.orTee` (method)
599+
600+
Like `andTee` for the error track. Takes a `Result<T, E>` and lets the `Err` value pass through regardless the result of the passed-in function.
601+
This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging.
602+
603+
**Signature:**
604+
605+
```typescript
606+
class Result<T, E> {
607+
orTee(
608+
callback: (value: E) => unknown
609+
): Result<T, E> { ... }
610+
}
611+
```
612+
613+
**Example:**
614+
615+
```typescript
616+
import { parseUserInput } from 'imaginary-parser'
617+
import { logParseError } from 'imaginary-logger'
618+
import { insertUser } from 'imaginary-database'
619+
620+
// ^ assume parseUserInput, logParseError and insertUser have the following signatures:
621+
// parseUserInput(input: RequestData): Result<User, ParseError>
622+
// logParseError(parseError: ParseError): Result<void, LogError>
623+
// insertUser(user: User): ResultAsync<void, InsertError>
624+
// Note logParseError returns void upon success but insertUser takes User type.
625+
626+
const resAsync = parseUserInput(userInput)
627+
.orTee(logParseError)
628+
.asyncAndThen(insertUser)
629+
630+
// Note no LogError shows up in the Result type
631+
resAsync.then((res: Result<void, ParseError | InsertError>) => {e
632+
if(res.isErr()){
633+
console.log("Oops, at least one step failed", res.error)
634+
}
635+
else{
636+
console.log("User input has been parsed and inserted successfully.")
637+
}
638+
}))
639+
```
640+
641+
[⬆️ Back to top](#toc)
642+
643+
---
644+
596645
#### `Result.andThrough` (method)
597646

598647
Similar to `andTee` except for:
@@ -1277,6 +1326,53 @@ resAsync.then((res: Result<void, InsertError | NotificationError>) => {e
12771326

12781327
[⬆️ Back to top](#toc)
12791328

1329+
---
1330+
#### `ResultAsync.orTee` (method)
1331+
1332+
Like `andTee` for the error track. Takes a `ResultAsync<T, E>` and lets the original `Err` value pass through regardless
1333+
the result of the passed-in function.
1334+
This is a handy way to handle side effects whose failure or success should not affect your main logics such as logging.
1335+
1336+
**Signature:**
1337+
1338+
```typescript
1339+
class ResultAsync<T, E> {
1340+
orTee(
1341+
callback: (value: E) => unknown
1342+
): ResultAsync<T, E> => { ... }
1343+
}
1344+
```
1345+
1346+
**Example:**
1347+
1348+
```typescript
1349+
import { insertUser } from 'imaginary-database'
1350+
import { logInsertError } from 'imaginary-logger'
1351+
import { sendNotification } from 'imaginary-service'
1352+
1353+
// ^ assume insertUser, logInsertError and sendNotification have the following signatures:
1354+
// insertUser(user: User): ResultAsync<User, InsertError>
1355+
// logInsertError(insertError: InsertError): Result<void, LogError>
1356+
// sendNotification(user: User): ResultAsync<void, NotificationError>
1357+
// Note logInsertError returns void on success but sendNotification takes User type.
1358+
1359+
const resAsync = insertUser(user)
1360+
.orTee(logUser)
1361+
.andThen(sendNotification)
1362+
1363+
// Note there is no LogError in the types below
1364+
resAsync.then((res: Result<void, InsertError | NotificationError>) => {e
1365+
if(res.isErr()){
1366+
console.log("Oops, at least one step failed", res.error)
1367+
}
1368+
else{
1369+
console.log("User has been inserted and notified successfully.")
1370+
}
1371+
}))
1372+
```
1373+
1374+
[⬆️ Back to top](#toc)
1375+
12801376
---
12811377
#### `ResultAsync.andThrough` (method)
12821378

src/result-async.ts

+16
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,22 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
130130
)
131131
}
132132

133+
orTee(f: (t: E) => unknown): ResultAsync<T, E> {
134+
return new ResultAsync(
135+
this._promise.then(async (res: Result<T, E>) => {
136+
if (res.isOk()) {
137+
return new Ok<T, E>(res.value)
138+
}
139+
try {
140+
await f(res.error)
141+
} catch (e) {
142+
// Tee does not care about the error
143+
}
144+
return new Err<T, E>(res.error)
145+
}),
146+
)
147+
}
148+
133149
mapErr<U>(f: (e: E) => U | Promise<U>): ResultAsync<T, U> {
134150
return new ResultAsync(
135151
this._promise.then(async (res: Result<T, E>) => {

src/result.ts

+25
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,18 @@ interface IResult<T, E> {
194194
*/
195195
andTee(f: (t: T) => unknown): Result<T, E>
196196

197+
/**
198+
* This "tee"s the current `Err` value to an passed-in computation such as side
199+
* effect functions but still returns the same `Err` value as the result.
200+
*
201+
* This is useful when you want to pass the current `Err` value to your side-track
202+
* work such as logging but want to continue error-track work after that.
203+
* This method does not care about the result of the passed in computation.
204+
*
205+
* @param f The function to apply to the current `Err` value
206+
*/
207+
orTee(f: (t: E) => unknown): Result<T, E>
208+
197209
/**
198210
* Similar to `andTee` except error result of the computation will be passed
199211
* to the downstream in case of an error.
@@ -342,6 +354,10 @@ export class Ok<T, E> implements IResult<T, E> {
342354
return ok<T, E>(this.value)
343355
}
344356

357+
orTee(_f: (t: E) => unknown): Result<T, E> {
358+
return ok<T, E>(this.value)
359+
}
360+
345361
orElse<R extends Result<unknown, unknown>>(
346362
_f: (e: E) => R,
347363
): Result<InferOkTypes<R> | T, InferErrTypes<R>>
@@ -428,6 +444,15 @@ export class Err<T, E> implements IResult<T, E> {
428444
return err(this.error)
429445
}
430446

447+
orTee(f: (t: E) => unknown): Result<T, E> {
448+
try {
449+
f(this.error)
450+
} catch (e) {
451+
// Tee doesn't care about the error
452+
}
453+
return err<T, E>(this.error)
454+
}
455+
431456
andThen<R extends Result<unknown, unknown>>(
432457
_f: (t: T) => R,
433458
): Result<InferOkTypes<R>, InferErrTypes<R> | E>

tests/index.test.ts

+50
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,31 @@ describe('Result.Ok', () => {
159159
})
160160
})
161161

162+
describe('orTee', () => {
163+
it('Calls the passed function but returns an original err', () => {
164+
const errVal = err(12)
165+
const passedFn = vitest.fn((_number) => {})
166+
167+
const teed = errVal.orTee(passedFn)
168+
169+
expect(teed.isErr()).toBe(true)
170+
expect(passedFn).toHaveBeenCalledTimes(1)
171+
expect(teed._unsafeUnwrapErr()).toStrictEqual(12)
172+
})
173+
it('returns an original err even when the passed function fails', () => {
174+
const errVal = err(12)
175+
const passedFn = vitest.fn((_number) => {
176+
throw new Error('OMG!')
177+
})
178+
179+
const teed = errVal.orTee(passedFn)
180+
181+
expect(teed.isErr()).toBe(true)
182+
expect(passedFn).toHaveBeenCalledTimes(1)
183+
expect(teed._unsafeUnwrapErr()).toStrictEqual(12)
184+
})
185+
})
186+
162187
describe('asyncAndThrough', () => {
163188
it('Calls the passed function but returns an original ok as Async', async () => {
164189
const okVal = ok(12)
@@ -1064,6 +1089,31 @@ describe('ResultAsync', () => {
10641089
})
10651090
})
10661091

1092+
describe('orTee', () => {
1093+
it('Calls the passed function but returns an original err', async () => {
1094+
const errVal = errAsync(12)
1095+
const passedFn = vitest.fn((_number) => {})
1096+
1097+
const teed = await errVal.orTee(passedFn)
1098+
1099+
expect(teed.isErr()).toBe(true)
1100+
expect(passedFn).toHaveBeenCalledTimes(1)
1101+
expect(teed._unsafeUnwrapErr()).toStrictEqual(12)
1102+
})
1103+
it('returns an original err even when the passed function fails', async () => {
1104+
const errVal = errAsync(12)
1105+
const passedFn = vitest.fn((_number) => {
1106+
throw new Error('OMG!')
1107+
})
1108+
1109+
const teed = await errVal.orTee(passedFn)
1110+
1111+
expect(teed.isErr()).toBe(true)
1112+
expect(passedFn).toHaveBeenCalledTimes(1)
1113+
expect(teed._unsafeUnwrapErr()).toStrictEqual(12)
1114+
})
1115+
})
1116+
10671117
describe('orElse', () => {
10681118
it('Skips orElse on an Ok value', async () => {
10691119
const okVal = okAsync(12)

0 commit comments

Comments
 (0)