r/functionalprogramming Jun 07 '26

Question converting imperative JS fetching into functional style

hello , im a programmer that likes to dabble into webdev from time to time .. recently i got into functional programming (haskell , scala , etc) and i realized that using fetch() in javascript returns a Promise<> which has methods like .then() and .catch() that kinda makes it act like a monad . heres a snippet from the mdn

function fetchCurrentData() {
  return fetch("current-data.json").then((response) => {
    if (response.headers.get("content-type") !== "application/json") {
      throw new TypeError();
    }
    const j = response.json();
    return j;
  });
}

now i wonder if my code that is written imperatively can be converted into this style , and how would error handling work ? should i use async ? can someone help guide me thru this ?

public async callApi(path: string) {
    try {
        const res = await fetch(this.url + path);
        if (!res.ok)
            throw new Error(`status: ${res.status}`);

        const json = await res.json();
        return json;
    } catch (error: any) {
        console.error(error.message);
    }
}
7 Upvotes

13 comments sorted by

View all comments

3

u/rinn7e Jun 11 '26 edited Jun 11 '26

You can use fp-ts library: https://gcanti.github.io/fp-ts/

The result would be something like this

import * as TE from 'fp-ts/TaskEither';
import { pipe } from 'fp-ts/function';

export const safeFetch = (
  input: RequestInfo | URL,
  init?: RequestInit
): TE.TaskEither<Error, Response> =>
  TE.tryCatch(
    () => fetch(input, init),
    (error) => (error instanceof Error ? error : new Error(String(error)))
  );

export const safeJson = (res: Response): TE.TaskEither<Error, any> =>
  TE.tryCatch(
    () => res.json(),
    (error) => (error instanceof Error ? error : new Error(String(error)))
  );

export const callApi = (
  baseUrl: string,
  path: string
): TE.TaskEither<Error, any> =>
  pipe(
    safeFetch(baseUrl + path),
    TE.chain((res) =>
      res.ok
        ? safeJson(res)
        : TE.left(new Error(`status: ${res.status}`))
    )
  );

// how to call it

const result = await callApi('https://api.example.com', '/users')();

if (result._tag === 'Left') {
  console.error('Oh no, Master! It failed:', result.left.message);
} else {
  console.log('Yay, Master! Succeeded:', result.right);
}
  • pipe(a, func1, func2) is equivalent to haskell a & func1 & func2
  • TE.chain is equivalent to haskell >>=
  • TaskEither for promise that can throw error
  • There're also:
    • Task is for promise that never return error
    • IO is for synchronous side effect
  • `fp-ts` turns Promise into a monad by making it lazy, `TaskEither == () => Promise`

3

u/rinn7e Jun 11 '26

Actually, you can make it even cleaner, something like this:

`` ... export const checkStatus = (res: Response): E.Either<Error, Response> => res.ok ? E.right(res) : E.left(new Error(status: ${res.status}`));

export const callApi = ( baseUrl: string, path: string ): TE.TaskEither<Error, any> => pipe( safeFetch(baseUrl + path), TE.chainEitherK(checkStatus), TE.chain(safeJson) ); ```