Handle Response Error of JSON API in TypeScript (using async/await)
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