Last active
December 24, 2022 09:41
-
-
Save yuya-takeyama/e4b9c2c5f85387734c39d4a44e674f8a to your computer and use it in GitHub Desktop.
Handle Response Error of JSON API in TypeScript (using async/await)
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 = async (res: Response): Promise<ResponseWithParsedJson> => { | |
const json = await res.json().catch(e => { | |
// When failed to parse JSON even though Content-Type is JSON | |
throw toFetchError( | |
new Error(`JSON parse error: ${ensureError(e).message}`), | |
res, | |
); | |
}); | |
if (res.ok) { | |
// With successful response (2xx or 3xx) | |
return Promise.resolve(toResponseWithParsedJson(res, json)); | |
} else { | |
// With error response (4xx, 5xx or anything) | |
return Promise.reject( | |
toFetchError( | |
ensureError(jsonErrorMessage(json)), | |
toResponseWithParsedJson(res, json), | |
), | |
); | |
} | |
}; | |
const parseIfJson = async (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 = { | |
async get(path: string) { | |
try { | |
const res = await fetch(`https://blog.yuyat.jp${path}`); | |
return parseJson(res); | |
} catch (err) { | |
return handleNetworkError(err); | |
} | |
}, | |
}; | |
(async () => { | |
// Application logic | |
try { | |
const res = await API.get('/archives/2533'); | |
console.log('Success!'); | |
console.log(res); | |
} catch (err) { | |
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