Skip to content

Commit 1c71194

Browse files
committed
Allow custom Error classes in beforeError hook
Fixes #1353
1 parent 030dfbb commit 1c71194

File tree

4 files changed

+151
-9
lines changed

4 files changed

+151
-9
lines changed

documentation/9-hooks.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,16 +403,22 @@ const instance = got.extend({
403403
**Default: `[]`**
404404

405405
```ts
406-
(error: RequestError) => Promisable<RequestError>
406+
(error: RequestError) => Promisable<Error>
407407
```
408408

409409
Called with a [`RequestError`](8-errors.md#requesterror) instance. The error is passed to the hook right before it's thrown.
410410

411-
This is especially useful when you want to have more detailed errors.
411+
This hook can return any `Error` instance, allowing you to:
412+
- Return custom error classes for better error handling in your application
413+
- Extend `RequestError` with additional properties
414+
- Return plain `Error` instances when you don't need Got-specific error information
415+
416+
This is especially useful when you want to have more detailed errors or maintain backward compatibility with existing error handling code.
412417

413418
```js
414419
import got from 'got';
415420

421+
// Modify and return the error
416422
await got('https://api.github.com/repos/sindresorhus/got/commits', {
417423
responseType: 'json',
418424
hooks: {
@@ -429,4 +435,27 @@ await got('https://api.github.com/repos/sindresorhus/got/commits', {
429435
]
430436
}
431437
});
438+
439+
// Return a custom error class
440+
class CustomAPIError extends Error {
441+
constructor(message, statusCode) {
442+
super(message);
443+
this.name = 'CustomAPIError';
444+
this.statusCode = statusCode;
445+
}
446+
}
447+
448+
await got('https://api.example.com/endpoint', {
449+
hooks: {
450+
beforeError: [
451+
error => {
452+
// Return a custom error for backward compatibility with your application
453+
return new CustomAPIError(
454+
error.message,
455+
error.response?.statusCode
456+
);
457+
}
458+
]
459+
}
460+
});
432461
```

source/core/index.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ const cacheableStore = new WeakableMap<string | StorageAdapter, CacheableRequest
136136

137137
const redirectCodes: ReadonlySet<number> = new Set([300, 301, 302, 303, 304, 307, 308]);
138138

139+
// Track errors that have been processed by beforeError hooks to preserve custom error types
140+
const errorsProcessedByHooks = new WeakSet<Error>();
141+
139142
const proxiedRequestEvents = [
140143
'socket',
141144
'connect',
@@ -631,8 +634,16 @@ export default class Request extends Duplex implements RequestEvents<Request> {
631634
}
632635
}
633636

634-
if (error !== null && !is.undefined(error) && !(error instanceof RequestError)) {
635-
error = new RequestError(error.message, error, this);
637+
// Preserve custom errors returned by beforeError hooks.
638+
// For other errors, wrap non-RequestError instances for consistency.
639+
if (error !== null && !is.undefined(error)) {
640+
const processedByHooks = error instanceof Error && errorsProcessedByHooks.has(error);
641+
642+
if (!processedByHooks && !(error instanceof RequestError)) {
643+
error = error instanceof Error
644+
? new RequestError(error.message, error, this)
645+
: new RequestError(String(error), {}, this);
646+
}
636647
}
637648

638649
callback(error);
@@ -1496,9 +1507,24 @@ export default class Request extends Duplex implements RequestEvents<Request> {
14961507
// Skip calling the hooks on purpose.
14971508
// See https://github.com/sindresorhus/got/issues/2103
14981509
} else if (this.options) {
1499-
for (const hook of this.options.hooks.beforeError) {
1500-
// eslint-disable-next-line no-await-in-loop
1501-
error = await hook(error);
1510+
const hooks = this.options.hooks.beforeError;
1511+
if (hooks.length > 0) {
1512+
for (const hook of hooks) {
1513+
// eslint-disable-next-line no-await-in-loop
1514+
error = await hook(error) as RequestError;
1515+
1516+
// Validate hook return value
1517+
if (!(error instanceof Error)) {
1518+
throw new TypeError(`The \`beforeError\` hook must return an Error instance. Received ${is.string(error) ? 'string' : String(typeof error)}.`);
1519+
}
1520+
}
1521+
1522+
// Mark this error as processed by hooks so _destroy preserves custom error types.
1523+
// Only mark non-RequestError errors, since RequestErrors are already preserved
1524+
// by the instanceof check in _destroy (line 642).
1525+
if (!(error instanceof RequestError)) {
1526+
errorsProcessedByHooks.add(error);
1527+
}
15021528
}
15031529
}
15041530
} catch (error_: any) {

source/core/options.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export type BeforeRequestHookContext = {
103103

104104
export type BeforeRequestHook = (options: NormalizedOptions, context: BeforeRequestHookContext) => Promisable<void | Response | ResponseLike>;
105105
export type BeforeRedirectHook = (updatedOptions: NormalizedOptions, plainResponse: PlainResponse) => Promisable<void>;
106-
export type BeforeErrorHook = (error: RequestError) => Promisable<RequestError>;
106+
export type BeforeErrorHook = (error: RequestError) => Promisable<Error>;
107107
export type BeforeRetryHook = (error: RequestError, retryCount: number) => Promisable<void>;
108108
export type BeforeCacheHook = (response: PlainResponse) => false | void;
109109
export type AfterResponseHook<ResponseType = unknown> = (response: Response<ResponseType>, retryWithMergedOptions: (options: OptionsInit) => never) => Promisable<Response | CancelableRequest<Response>>;
@@ -296,13 +296,20 @@ export type Hooks = {
296296
/**
297297
Called with a `RequestError` instance. The error is passed to the hook right before it's thrown.
298298
299-
This is especially useful when you want to have more detailed errors.
299+
This hook can return any `Error` instance, allowing you to:
300+
- Return custom error classes for better error handling in your application
301+
- Extend `RequestError` with additional properties
302+
- Return plain `Error` instances when you don't need Got-specific error information
303+
304+
This is especially useful when you want to have more detailed errors or maintain backward compatibility with existing error handling code.
300305
301306
@default []
302307
308+
@example
303309
```
304310
import got from 'got';
305311
312+
// Modify and return the error
306313
await got('https://api.github.com/repos/sindresorhus/got/commits', {
307314
responseType: 'json',
308315
hooks: {
@@ -319,6 +326,29 @@ export type Hooks = {
319326
]
320327
}
321328
});
329+
330+
// Return a custom error class
331+
class CustomAPIError extends Error {
332+
constructor(message, statusCode) {
333+
super(message);
334+
this.name = 'CustomAPIError';
335+
this.statusCode = statusCode;
336+
}
337+
}
338+
339+
await got('https://api.example.com/endpoint', {
340+
hooks: {
341+
beforeError: [
342+
error => {
343+
// Return a custom error for backward compatibility with your application
344+
return new CustomAPIError(
345+
error.message,
346+
error.response?.statusCode
347+
);
348+
}
349+
]
350+
}
351+
});
322352
```
323353
*/
324354
beforeError: BeforeErrorHook[];

test/hooks.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,3 +2226,60 @@ test('multiple handlers with error transformation work with .json()', withServer
22262226
// Should get error from first handler (outermost)
22272227
await t.throwsAsync(instance('').json(), {message: /Handler 1: Handler 2:/});
22282228
});
2229+
2230+
test('beforeError can return custom Error class', async t => {
2231+
class CustomError extends Error {
2232+
constructor(message: string) {
2233+
super(message);
2234+
this.name = 'CustomError';
2235+
}
2236+
}
2237+
2238+
const customMessage = 'This is a custom error';
2239+
2240+
const error = await t.throwsAsync(
2241+
got('https://example.com', {
2242+
request() {
2243+
throw new Error('Original error');
2244+
},
2245+
hooks: {
2246+
beforeError: [
2247+
() => new CustomError(customMessage),
2248+
],
2249+
},
2250+
}),
2251+
);
2252+
2253+
t.is(error?.name, 'CustomError');
2254+
t.is(error?.message, customMessage);
2255+
t.true(error instanceof CustomError);
2256+
});
2257+
2258+
test('beforeError can extend RequestError with custom error', async t => {
2259+
class MyCustomError extends RequestError {
2260+
constructor(message: string, error: Error, request: any) {
2261+
super(message, error, request);
2262+
this.name = 'MyCustomError';
2263+
}
2264+
}
2265+
2266+
const customMessage = 'Custom RequestError';
2267+
2268+
const error = await t.throwsAsync(
2269+
got('https://example.com', {
2270+
request() {
2271+
throw new Error('Original error');
2272+
},
2273+
hooks: {
2274+
beforeError: [
2275+
error => new MyCustomError(customMessage, error, error.request),
2276+
],
2277+
},
2278+
}),
2279+
);
2280+
2281+
t.is(error?.name, 'MyCustomError');
2282+
t.is(error?.message, customMessage);
2283+
t.true(error instanceof MyCustomError);
2284+
t.true(error instanceof RequestError);
2285+
});

0 commit comments

Comments
 (0)