Last active
February 19, 2025 09:28
-
-
Save yuya-takeyama/2a18d166b43101ca77a461fded8916c9 to your computer and use it in GitHub Desktop.
Handle Response Error of JSON API in TypeScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fetch, { Response } from 'node-fetch'; | |
interface ResponseWithParsedJson extends Response { | |
parsedJson?: any; | |
} | |
const toResponseWithParsedJson = ( | |
res: Response, | |
json: any, | |
): ResponseWithParsedJson => { | |
const _res = res.clone() as ResponseWithParsedJson; | |
_res.parsedJson = json; | |
return _res; | |
}; | |
export type FetchError = | |
| NetworkError | |
| RequestError | |
| ServerError | |
| UnknownError; | |
export interface NetworkError extends Error { | |
isNetworkError: boolean; | |
} | |
export interface RequestError extends ErrorWithResponse { | |
isRequestError: boolean; | |
} | |
export interface ServerError extends ErrorWithResponse { | |
isServerError: boolean; | |
} | |
export interface UnknownError extends ErrorWithResponse { | |
isUnknownError: boolean; | |
} | |
export interface ErrorWithResponse extends Error { | |
response: ResponseWithParsedJson; | |
} | |
export const isFetchError = (err: FetchError | any): err is FetchError => { | |
return ( | |
isNetworkError(err) || | |
isRequestError(err) || | |
isServerError(err) || | |
isUnknownError(err) | |
); | |
}; | |
export const isNetworkError = ( | |
err: NetworkError | any, | |
): err is NetworkError => { | |
return !!(err instanceof Error && (err as NetworkError).isNetworkError); | |
}; | |
export const isRequestError = ( | |
err: RequestError | any, | |
): err is RequestError => { | |
return !!(err instanceof Error && (err as RequestError).isRequestError); | |
}; | |
export const isServerError = (err: ServerError | any): err is ServerError => { | |
return !!(err instanceof Error && (err as ServerError).isServerError); | |
}; | |
export const isUnknownError = ( | |
err: UnknownError | any, | |
): err is UnknownError => { | |
return !!(err instanceof Error && (err as UnknownError).isUnknownError); | |
}; | |
const toFetchError = (err: Error, res: ResponseWithParsedJson): FetchError => { | |
(err as ErrorWithResponse).response = res; | |
if (res.status >= 400 && res.status < 500) { | |
(err as RequestError).isRequestError = true; | |
return err as RequestError; | |
} else if (res.status >= 500 && res.status < 600) { | |
(err as ServerError).isServerError = true; | |
return err as ServerError; | |
} else { | |
(err as UnknownError).isUnknownError = true; | |
return err as UnknownError; | |
} | |
}; | |
const ensureError = (err: Error | string | any): Error => { | |
if (err instanceof Error) { | |
return err; | |
} else if (typeof err === 'string') { | |
return new Error(err); | |
} else { | |
return new Error('Unknown error'); | |
} | |
}; | |
const hasJsonContent = (res: Response): boolean => { | |
const contentType = res.headers.get('Content-Type'); | |
return !!(contentType && contentType.includes('application/json')); | |
}; | |
const jsonErrorMessage = (json: any): string | undefined => { | |
if (typeof json === 'object' && typeof json.message === 'string') { | |
return json.message; | |
} | |
}; | |
const parseJson = (res: Response): Promise<ResponseWithParsedJson> => { | |
return new Promise((resolve, reject) => { | |
res | |
.json() | |
.catch(e => { | |
// When failed to parse JSON even though Content-Type is JSON | |
reject( | |
toFetchError( | |
new Error(`JSON parse error: ${ensureError(e).message}`), | |
res, | |
), | |
); | |
}) | |
.then(json => { | |
if (res.ok) { | |
// With successful response (2xx or 3xx) | |
resolve(toResponseWithParsedJson(res, json)); | |
} else { | |
// With error response (4xx, 5xx or anything) | |
reject( | |
toFetchError( | |
ensureError(jsonErrorMessage(json)), | |
toResponseWithParsedJson(res, json), | |
), | |
); | |
} | |
}); | |
}); | |
}; | |
const parseIfJson = (res: Response): Promise<ResponseWithParsedJson> => { | |
if (hasJsonContent(res)) { | |
return parseJson(res); | |
} else { | |
if (res.ok) { | |
// With successful response (2xx or 3xx) | |
return Promise.resolve(res); | |
} else { | |
// With error response (4xx, 5xx or anything) | |
return Promise.reject( | |
toFetchError(new Error(`HTTP ${res.status}: ${res.statusText}`), res), | |
); | |
} | |
} | |
}; | |
const handleNetworkError = (err: Error) => { | |
(err as NetworkError).isNetworkError = true; | |
return Promise.reject(err); | |
}; | |
// Global response handler | |
const API = { | |
get(path: string) { | |
return fetch(`https://blog.yuyat.jp${path}`) | |
.catch(handleNetworkError) | |
.then(parseIfJson); | |
}, | |
}; | |
// Application logic | |
API.get('/archives/2533') | |
.then(res => { | |
console.log('Success!'); | |
console.log(res); | |
}) | |
.catch((err: FetchError | Error) => { | |
if (isNetworkError(err)) { | |
console.error(err); | |
} else if (isRequestError(err)) { | |
console.error(err); | |
} else if (isServerError(err)) { | |
console.error(err); | |
} else { | |
console.error(err); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment