Repository: sindresorhus/got
Pull request #696 (2019-01-12 11:01:53)

Add `beforeError` hook
Fixes https://github.com/sindresorhus/gh-got/blob/master/index.js

Instead using handler we could do:

```js
hooks: {
	beforeRequest: [
		options => {
			if (options.token) {
				options.headers.authorization = options.headers.authorization || `token ${options.token}`;
			}
		}
	],
	afterResponse: [
		response => {
			response.rateLimit = getRateLimit(response);
			return response;
		}
	],
	onError: [
		error => {
			const {response} = error;

			if (response && response.body) {
				error.name = 'GitHubError';
				error.message = `${response.body.message} (${error.statusCode})`;
			}

			if (response) {
				error.rateLimit = getRateLimit(response);
			}

			return error;
		}
	]
}
```

#### Checklist

- [x] I have read the documentation.
- [x] I have included a pull request description of my changes.
- [x] I have included some tests.
- [x] If it's a new feature, I have included documentation updates.


sindresorhus: > Fixes sindresorhus/gh-got:index.js@master

I wouldn't say it fixes that issue. This just works around it. We still need to properly fix `handler`.
sindresorhus: Does the hook catch absolutely *all* errors? If not, it should be documented what it doesn't catch. For example, I don't think it currently catches input validation errors (Neither should it IMHO).
sindresorhus: Wouldn't `beforeError` be a more correct name for this?
sindresorhus: Can you fix the merge conflict?
szmarczak: done

Patch:
diff --git a/advanced-creation.md b/advanced-creation.md
index 67a0b60..a03bce2 100644
--- a/advanced-creation.md
+++ b/advanced-creation.md
@@ -98,6 +98,7 @@ const defaults = {
 			'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
 		},
 		hooks: {
+			beforeError: [],
 			init: [],
 			beforeRequest: [],
 			beforeRedirect: [],
diff --git a/readme.md b/readme.md
index f4b4e10..37132ab 100644
--- a/readme.md
+++ b/readme.md
@@ -458,6 +458,35 @@ const instance = got.extend({
 });
 ```
 
+###### hooks.beforeError
+
+Type: `Function[]`<br>
+Default: `[]`
+
+Called with an `Error` instance. The error is passed to the hook right before it's thrown. This is especially useful when you want to have more detailed errors.
+
+**Note**: Errors thrown while normalizing input options are thrown directly and not part of this hook.
+
+```js	
+const got = require('got');	
+
+got('api.github.com/some-endpoint', {	
+	hooks: {	
+		onError: [	
+			error => {	
+				const {response} = error;	
+ 				if (response && response.body) {	
+					error.name = 'GitHubError';	
+					error.message = `${response.body.message} (${error.statusCode})`;	
+				}
+
+ 				return error;	
+			}	
+		]	
+	}	
+});	
+```
+
 #### Response
 
 The response object will typically be a [Node.js HTTP response stream](https://nodejs.org/api/http.html#http_class_http_incomingmessage), however, if returned from the cache it will be a [response-like object](https://github.com/lukechilds/responselike) which behaves in the same way.
diff --git a/source/known-hook-events.js b/source/known-hook-events.js
index a991073..cd245e1 100644
--- a/source/known-hook-events.js
+++ b/source/known-hook-events.js
@@ -1,6 +1,7 @@
 'use strict';
 
 module.exports = [
+	'beforeError',
 	'init',
 	'beforeRequest',
 	'beforeRedirect',
diff --git a/source/request-as-event-emitter.js b/source/request-as-event-emitter.js
index 7104627..79586af 100644
--- a/source/request-as-event-emitter.js
+++ b/source/request-as-event-emitter.js
@@ -33,6 +33,19 @@ module.exports = (options, input) => {
 	const getCookieString = options.cookieJar ? util.promisify(options.cookieJar.getCookieString.bind(options.cookieJar)) : null;
 	const agents = is.object(options.agent) ? options.agent : null;
 
+	const emitError = async error => {
+		try {
+			for (const hook of options.hooks.beforeError) {
+				// eslint-disable-next-line no-await-in-loop
+				error = await hook(error);
+			}
+
+			emitter.emit('error', error);
+		} catch (error2) {
+			emitter.emit('error', error2);
+		}
+	};
+
 	const get = async options => {
 		const currentUrl = redirectString || requestUrl;
 
@@ -141,7 +154,7 @@ module.exports = (options, input) => {
 
 				getResponse(response, options, emitter);
 			} catch (error) {
-				emitter.emit('error', error);
+				emitError(error);
 			}
 		};
 
@@ -166,7 +179,7 @@ module.exports = (options, input) => {
 				}
 
 				if (emitter.retry(error) === false) {
-					emitter.emit('error', error);
+					emitError(error);
 				}
 			});
 
@@ -198,7 +211,7 @@ module.exports = (options, input) => {
 					request.end(uploadComplete);
 				}
 			} catch (error) {
-				emitter.emit('error', new RequestError(error, options));
+				emitError(new RequestError(error, options));
 			}
 		};
 
@@ -208,9 +221,9 @@ module.exports = (options, input) => {
 
 			cacheRequest.once('error', error => {
 				if (error instanceof CacheableRequest.RequestError) {
-					emitter.emit('error', new RequestError(error, options));
+					emitError(new RequestError(error, options));
 				} else {
-					emitter.emit('error', new CacheError(error, options));
+					emitError(new CacheError(error, options));
 				}
 			});
 
@@ -220,7 +233,7 @@ module.exports = (options, input) => {
 			try {
 				handleRequest(fn.request(options, handleResponse));
 			} catch (error) {
-				emitter.emit('error', new RequestError(error, options));
+				emitError(new RequestError(error, options));
 			}
 		}
 	};
@@ -231,7 +244,7 @@ module.exports = (options, input) => {
 		try {
 			backoff = options.retry.retries(++retryCount, error);
 		} catch (error2) {
-			emitter.emit('error', error2);
+			emitError(error2);
 			return;
 		}
 
@@ -245,7 +258,7 @@ module.exports = (options, input) => {
 
 					await get(options);
 				} catch (error) {
-					emitter.emit('error', error);
+					emitError(error);
 				}
 			};
 
@@ -291,7 +304,7 @@ module.exports = (options, input) => {
 
 			await get(options);
 		} catch (error) {
-			emitter.emit('error', error);
+			emitError(error);
 		}
 	});
 


Test patch:
diff --git a/test/hooks.js b/test/hooks.js
index bc11587..ee4b136 100644
--- a/test/hooks.js
+++ b/test/hooks.js
@@ -169,6 +169,15 @@ test('catches afterResponse promise rejections', async t => {
 	}), errorString);
 });
 
+test('catches beforeError errors', async t => {
+	await t.throwsAsync(() => got(s.url, {
+		request: () => {},
+		hooks: {
+			beforeError: [() => Promise.reject(error)]
+		}
+	}), errorString);
+});
+
 test('init is called with options', async t => {
 	await got(s.url, {
 		json: true,
@@ -399,3 +408,32 @@ test.serial('doesn\'t throw on afterResponse retry HTTP failure if throwHttpErro
 	});
 	t.is(statusCode, 500);
 });
+
+test('beforeError is called with an error', async t => {
+	await t.throwsAsync(() => got(s.url, {
+		request: () => {
+			throw error;
+		},
+		hooks: {
+			beforeError: [error2 => {
+				t.true(error2 instanceof Error);
+				return error2;
+			}]
+		}
+	}), errorString);
+});
+
+test('beforeError allows modifications', async t => {
+	const errorString2 = 'foobar';
+
+	await t.throwsAsync(() => got(s.url, {
+		request: () => {
+			throw error;
+		},
+		hooks: {
+			beforeError: [() => {
+				return new Error(errorString2);
+			}]
+		}
+	}), errorString2);
+});