Skip to content

Commit f6ba725

Browse files
Yoni GoldbergYoni Goldberg
Yoni Goldberg
authored and
Yoni Goldberg
committed
Error handling section
1 parent 92ad4d9 commit f6ba725

File tree

2 files changed

+62
-65
lines changed

2 files changed

+62
-65
lines changed

README.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ Read in a different language: [![CN](./assets/flags/CN.png)**CN**](./README.chin
7070
</summary>
7171

7272
&emsp;&emsp;[2.1 Use Async-Await or promises for async error handling](#-21-use-async-await-or-promises-for-async-error-handling)</br>
73-
&emsp;&emsp;[2.2 Use only the built-in Error object `#strategic`](#-22-use-only-the-built-in-error-object)</br>
74-
&emsp;&emsp;[2.3 Distinguish operational vs programmer errors `#strategic`](#-23-distinguish-operational-vs-programmer-errors)</br>
73+
&emsp;&emsp;[2.2 Extend the built-in Error object `#strategic #updated`](#-22-extend-the-built-in-error-object)</br>
74+
&emsp;&emsp;[2.3 Distinguish operational vs programmer errors `#strategic` `#updated`](#-23-distinguish-catastrophic-errors-from-operational-errors)</br>
7575
&emsp;&emsp;[2.4 Handle errors centrally, not within a middleware `#strategic`](#-24-handle-errors-centrally-not-within-a-middleware)</br>
76-
&emsp;&emsp;[2.5 Document API errors using Swagger or GraphQL `#modified-recently`](#-25-document-api-errors-using-swagger-or-graphql)</br>
76+
&emsp;&emsp;[2.5 Document API errors using OpenAPI or GraphQL `#updated`](#-25-document-api-errors-using-openapi-or -graphql)</br>
7777
&emsp;&emsp;[2.6 Exit the process gracefully when a stranger comes to town `#strategic`](#-26-exit-the-process-gracefully-when-a-stranger-comes-to-town)</br>
78-
&emsp;&emsp;[2.7 Use a mature logger to increase error visibility](#-27-use-a-mature-logger-to-increase-error-visibility)</br>
78+
&emsp;&emsp;[2.7 Use a mature logger to increase errors visibility `#updated`](#-27-use-a-mature-logger-to-increase-errors-visibility)</br>
7979
&emsp;&emsp;[2.8 Test error flows using your favorite test framework](#-28-test-error-flows-using-your-favorite-test-framework)</br>
8080
&emsp;&emsp;[2.9 Discover errors and downtime using APM products](#-29-discover-errors-and-downtime-using-apm-products)</br>
8181
&emsp;&emsp;[2.10 Catch unhandled promise rejections `#modified-recently`](#-210-catch-unhandled-promise-rejections)</br>
@@ -309,47 +309,47 @@ my-system
309309

310310
## ![] 2.1 Use Async-Await or promises for async error handling
311311

312-
**TL;DR:** Handling async errors in callback style is probably the fastest way to hell (a.k.a the pyramid of doom). The best gift you can give to your code is using a reputable promise library or async-await instead which enables a much more compact and familiar code syntax like try-catch
312+
**TL;DR:** Handling async errors in callback style is probably the fastest way to hell (a.k.a the pyramid of doom). The best gift you can give to your code is using Promises with async-await which enables a much more compact and familiar code syntax like try-catch
313313

314314
**Otherwise:** Node.js callback style, function(err, response), is a promising way to un-maintainable code due to the mix of error handling with casual code, excessive nesting, and awkward coding patterns
315315

316316
🔗 [**Read More: avoiding callbacks**](./sections/errorhandling/asyncerrorhandling.md)
317317

318318
<br/><br/>
319319

320-
## ![] 2.2 Use only the built-in Error object
320+
## ![] 2.2 Extend the built-in Error object
321321

322-
**TL;DR:** Many throw errors as a string or as some custom type – this complicates the error handling logic and the interoperability between modules. Whether you reject a promise, throw an exception or emit an error – using only the built-in Error object (or an object that extends the built-in Error object) will increase uniformity and prevent loss of information. There is `no-throw-literal` ESLint rule that strictly checks that (although it has some [limitations](https://eslint.org/docs/rules/no-throw-literal) which can be solved when using TypeScript and setting the `@typescript-eslint/no-throw-literal` rule)
322+
**TL;DR:** Some libraries throw errors as a string or as some custom type – this complicates the error handling logic and the interoperability between modules. Instead, create app error object/class that extends the built-in Error object and use it whenever rejecting, throwing or emitting an error. The app error should add useful imperative properties like the error name/code and isCatastrophic. By doing so, all errors have a unified structure and support better error handling .There is `no-throw-literal` ESLint rule that strictly checks that (although it has some [limitations](https://eslint.org/docs/rules/no-throw-literal) which can be solved when using TypeScript and setting the `@typescript-eslint/no-throw-literal` rule)
323323

324324
**Otherwise:** When invoking some component, being uncertain which type of errors come in return – it makes proper error handling much harder. Even worse, using custom types to describe errors might lead to loss of critical error information like the stack trace!
325325

326326
🔗 [**Read More: using the built-in error object**](./sections/errorhandling/useonlythebuiltinerror.md)
327327

328328
<br/><br/>
329329

330-
## ![] 2.3 Distinguish operational vs programmer errors
330+
## ![] 2.3 Distinguish catastrophic errors from operational errors
331331

332-
**TL;DR:** Operational errors (e.g. API received an invalid input) refer to known cases where the error impact is fully understood and can be handled thoughtfully. On the other hand, programmer error (e.g. trying to read an undefined variable) refers to unknown code failures that dictate to gracefully restart the application
332+
**TL;DR:** Operational errors (e.g. API received an invalid input) refer to known cases where the error impact is fully understood and can be handled thoughtfully. On the other hand, catastrophic error (also known as programmer errors) refers to unusual code failures that dictate to gracefully restart the application
333333

334-
**Otherwise:** You may always restart the application when an error appears, but why let ~5000 online users down because of a minor, predicted, operational error? The opposite is also not ideal – keeping the application up when an unknown issue (programmer error) occurred might lead to an unpredicted behavior. Differentiating the two allows acting tactfully and applying a balanced approach based on the given context
334+
**Otherwise:** You may always restart the application when an error appears, but why let ~5000 online users down because of a minor, predicted, operational error? The opposite is also not ideal – keeping the application up when an unknown catastrophic issue (programmer error) occurred might lead to an unpredicted behavior. Differentiating the two allows acting tactfully and applying a balanced approach based on the given context
335335

336336
🔗 [**Read More: operational vs programmer error**](./sections/errorhandling/operationalvsprogrammererror.md)
337337

338338
<br/><br/>
339339

340340
## ![] 2.4 Handle errors centrally, not within a middleware
341341

342-
**TL;DR:** Error handling logic such as mail to admin and logging should be encapsulated in a dedicated and centralized object that all endpoints (e.g. Express middleware, cron jobs, unit-testing) call when an error comes in
342+
**TL;DR:** Error handling logic such as logging, deciding whether to crash and monitoring metrics should be encapsulated in a dedicated and centralized object that all entry-points (e.g. APIs, cron jobs, scheduled jobs) call when an error comes in
343343

344344
**Otherwise:** Not handling errors within a single place will lead to code duplication and probably to improperly handled errors
345345

346346
🔗 [**Read More: handling errors in a centralized place**](./sections/errorhandling/centralizedhandling.md)
347347

348348
<br/><br/>
349349

350-
## ![] 2.5 Document API errors using Swagger or GraphQL
350+
## ![] 2.5 Document API errors using OpenAPI or GraphQL
351351

352-
**TL;DR:** Let your API callers know which errors might come in return so they can handle these thoughtfully without crashing. For RESTful APIs, this is usually done with documentation frameworks like Swagger. If you're using GraphQL, you can utilize your schema and comments as well.
352+
**TL;DR:** Let your API callers know which errors might come in return so they can handle these thoughtfully without crashing. For RESTful APIs, this is usually done with documentation frameworks like OpenAPI. If you're using GraphQL, you can utilize your schema and comments as well
353353

354354
**Otherwise:** An API client might decide to crash and restart only because it received back an error it couldn’t understand. Note: the caller of your API might be you (very typical in a microservice environment)
355355

@@ -359,17 +359,17 @@ my-system
359359

360360
## ![] 2.6 Exit the process gracefully when a stranger comes to town
361361

362-
**TL;DR:** When an unknown error occurs (a developer error, see best practice 2.3) - there is uncertainty about the application healthiness. Common practice suggests restarting the process carefully using a process management tool like [Forever](https://www.npmjs.com/package/forever) or [PM2](http://pm2.keymetrics.io/)
362+
**TL;DR:** When an unknown error occurs (catastrophic error, see best practice 2.3) - there is uncertainty about the application healthiness. In this case, there is no escape from making the error observable, shutting off connections and exiting the process. Any reputable runtime framework like Dockerized services or cloud serverless solutions will take care to restart
363363

364364
**Otherwise:** When an unfamiliar exception occurs, some object might be in a faulty state (e.g. an event emitter which is used globally and not firing events anymore due to some internal failure) and all future requests might fail or behave crazily
365365

366366
🔗 [**Read More: shutting the process**](./sections/errorhandling/shuttingtheprocess.md)
367367

368368
<br/><br/>
369369

370-
## ![] 2.7 Use a mature logger to increase error visibility
370+
## ![] 2.7 Use a mature logger to increase errors visibility
371371

372-
**TL;DR:** A set of mature logging tools like [Pino](https://github.com/pinojs/pino) or [Log4js](https://www.npmjs.com/package/log4js), will speed-up error discovery and understanding. So forget about console.log
372+
**TL;DR:** A robust logging tools like [Pino](https://github.com/pinojs/pino) or [Winston](https://github.com/winstonjs/winston) increases the errors visibility using features like log-levels, pretty print coloring and more. Console.log lacks these imperative features and should be avoided. The best in class logger allows attaching custom useful properties to log entries with minimized serialization performance penalty. Developers should write logs to `stdout` and let the infrastructure pipe the stream to the appropriate log aggregator
373373

374374
**Otherwise:** Skimming through console.logs or manually through messy text file without querying tools or a decent log viewer might keep you busy at work until late
375375

@@ -379,7 +379,7 @@ my-system
379379

380380
## ![] 2.8 Test error flows using your favorite test framework
381381

382-
**TL;DR:** Whether professional automated QA or plain manual developer testing – Ensure that your code not only satisfies positive scenarios but also handles and returns the right errors. Testing frameworks like Mocha & Chai can handle this easily (see code examples within the "Gist popup")
382+
**TL;DR:** Whether professional automated QA or plain manual developer testing – Ensure that your code not only satisfies positive scenarios but also handles and returns the right errors. On top of this, simulate deeper error flows like uncaught exceptions an ensure that the error handler treat these properly (see code examples within the "read more" section)
383383

384384
**Otherwise:** Without testing, whether automatically or manually, you can’t rely on your code to return the right errors. Without meaningful errors – there’s no error handling
385385

@@ -409,7 +409,7 @@ my-system
409409

410410
## ![] 2.11 Fail fast, validate arguments using a dedicated library
411411

412-
**TL;DR:** Assert API input to avoid nasty bugs that are much harder to track later. The validation code is usually tedious unless you are using a very cool helper library like [ajv](https://www.npmjs.com/package/ajv) and [Joi](https://www.npmjs.com/package/joi)
412+
**TL;DR:** Assert API input to avoid nasty bugs that are much harder to track later. The validation code is usually tedious unless you are using a modern validation library like [ajv](https://www.npmjs.com/package/ajv), [zod](https://github.com/colinhacks/zod), or [typebox](https://github.com/sinclairzx81/typebox)
413413

414414
**Otherwise:** Consider this – your function expects a numeric argument “Discount” which the caller forgets to pass, later on, your code checks if Discount!=0 (amount of allowed discount is greater than zero), then it will allow the user to enjoy a discount. OMG, what a nasty bug. Can you see it?
415415

sections/errorhandling/testingerrorflows.md

Lines changed: 44 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,72 +10,69 @@ Testing ‘happy’ paths is no better than testing failures. Good testing code
1010
<summary><strong>Javascript</strong></summary>
1111

1212
```javascript
13-
describe('Facebook chat', () => {
14-
it('Notifies on new chat message', () => {
13+
describe("Facebook chat", () => {
14+
it("Notifies on new chat message", () => {
1515
const chatService = new chatService();
1616
chatService.participants = getDisconnectedParticipants();
17-
expect(chatService.sendMessage.bind({ message: 'Hi' })).to.throw(ConnectionError);
17+
expect(chatService.sendMessage.bind({ message: "Hi" })).to.throw(ConnectionError);
1818
});
1919
});
2020
```
21+
2122
</details>
2223

24+
### Code example: ensuring API returns the right HTTP error code and log properly
25+
2326
<details>
24-
<summary><strong>Typescript</strong></summary>
27+
<summary><strong>Javascript</strong></summary>
2528

26-
```typescript
27-
describe('Facebook chat', () => {
28-
it('Notifies on new chat message', () => {
29-
const chatService = new chatService();
30-
chatService.participants = getDisconnectedParticipants();
31-
expect(chatService.sendMessage.bind({ message: 'Hi' })).to.throw(ConnectionError);
29+
```javascript
30+
test("When exception is throw during request, Then logger reports the mandatory fields", async () => {
31+
//Arrange
32+
const orderToAdd = {
33+
userId: 1,
34+
productId: 2,
35+
};
36+
37+
sinon
38+
.stub(OrderRepository.prototype, "addOrder")
39+
.rejects(new AppError("saving-failed", "Order could not be saved", 500));
40+
const loggerDouble = sinon.stub(logger, "error");
41+
42+
//Act
43+
const receivedResponse = await axiosAPIClient.post("/order", orderToAdd);
44+
45+
//Assert
46+
expect(receivedResponse.status).toBe(500);
47+
expect(loggerDouble.lastCall.firstArg).toMatchObject({
48+
name: "saving-failed",
49+
status: 500,
50+
stack: expect.any(String),
51+
message: expect.any(String),
3252
});
3353
});
3454
```
55+
3556
</details>
3657

37-
### Code example: ensuring API returns the right HTTP error code
58+
### Code example: ensuring that are uncaught exceptions are handled as well
3859

3960
<details>
4061
<summary><strong>Javascript</strong></summary>
4162

4263
```javascript
43-
it('Creates new Facebook group', () => {
44-
const invalidGroupInfo = {};
45-
return httpRequest({
46-
method: 'POST',
47-
uri: 'facebook.com/api/groups',
48-
resolveWithFullResponse: true,
49-
body: invalidGroupInfo,
50-
json: true
51-
}).then((response) => {
52-
expect.fail('if we were to execute the code in this block, no error was thrown in the operation above')
53-
}).catch((response) => {
54-
expect(400).to.equal(response.statusCode);
55-
});
56-
});
57-
```
58-
</details>
64+
test("When unhandled exception is throw, Then the logger reports correctly", async () => {
65+
//Arrange
66+
await api.startWebServer();
67+
const loggerDouble = sinon.stub(logger, "error");
68+
const errorToThrow = new Error("An error that wont be caught 😳");
5969

60-
<details>
61-
<summary><strong>Typescript</strong></summary>
62-
63-
```typescript
64-
it('Creates new Facebook group', async () => {
65-
let invalidGroupInfo = {};
66-
try {
67-
const response = await httpRequest({
68-
method: 'POST',
69-
uri: 'facebook.com/api/groups',
70-
resolveWithFullResponse: true,
71-
body: invalidGroupInfo,
72-
json: true
73-
})
74-
// if we were to execute the code in this block, no error was thrown in the operation above
75-
expect.fail('The request should have failed')
76-
} catch(response) {
77-
expect(400).to.equal(response.statusCode);
78-
}
70+
//Act
71+
process.emit("uncaughtException", errorToThrow);
72+
73+
// Assert
74+
expect(loggerDouble.calledWith(errorToThrow));
7975
});
8076
```
81-
</details>
77+
78+
</details>

0 commit comments

Comments
 (0)