Sometimes you don’t want to recover from an error. You just want to log it, send it to an error tracking service, increment a metric, then let it continue propagating.
HttpClient.tapError() runs a side effect when an error occurs, without changing the error or preventing it from propagating.
import { FetchHttpClient, HttpClient, HttpClientRequest,} from "effect/unstable/http";import { Effect } from "effect";
function fetchUserWithErrorTracking(userId: number) { return Effect.gen(function* () { const client = (yield* HttpClient.HttpClient).pipe( HttpClient.filterStatusOk, HttpClient.tapError((error) => Effect.sync(() => { // Simulate sending to an error tracking service console.log("[ErrorTracker] Reported:", error._tag, error.message); }), ), );
const request = HttpClientRequest.get( `https://dummyjson.com/users/${userId}`, );
const response = yield* client.execute(request); const user = yield* response.json;
return user; }).pipe(Effect.provide(FetchHttpClient.layer));}
// Test with an invalid user — error is tracked, then still propagatesEffect.runPromise(fetchUserWithErrorTracking(9999)).then( (user) => console.log("User:", user.firstName), (error) => console.log("[Main] Unhandled error:", error.message),);Output:
[ErrorTracker] Reported: HttpClientError StatusCode: non 2xx status code (404 GET https://dummyjson.com/users/9999)[Main] Unhandled error: StatusCode: non 2xx status code (404 GET https://dummyjson.com/users/9999)Notice both lines print. The tapError callback runs first, then the error continues to propagate. It’s not swallowed.
tapError is particularly useful for observability. You can add it to a client once, and every request through that client will have its errors observed without affecting the error handling strategy of the calling code.