Merge pull request #584 from kevynb/typescript-examples

Add typescript examples for error handling and project structure
This commit is contained in:
Yoni Goldberg
2019-11-17 21:14:38 +02:00
committed by GitHub
10 changed files with 391 additions and 41 deletions

View File

@ -8,13 +8,14 @@ Callbacks dont scale well since most programmers are not familiar with them.
```javascript ```javascript
return functionA() return functionA()
.then((valueA) => functionB(valueA)) .then(functionB)
.then((valueB) => functionC(valueB)) .then(functionC)
.then((valueC) => functionD(valueC)) .then(functionD)
.catch((err) => logger.error(err)) .catch((err) => logger.error(err))
.then(alwaysExecuteThisFunction()) .then(alwaysExecuteThisFunction)
``` ```
### Code Example - using async/await to catch errors ### Code Example - using async/await to catch errors
```javascript ```javascript
@ -25,7 +26,7 @@ async function executeAsyncTask () {
const valueC = await functionC(valueB); const valueC = await functionC(valueB);
return await functionD(valueC); return await functionD(valueC);
} }
catch(err) { catch (err) {
logger.error(err); logger.error(err);
} finally { } finally {
await alwaysExecuteThisFunction(); await alwaysExecuteThisFunction();
@ -35,6 +36,9 @@ async function executeAsyncTask () {
### Anti pattern code example callback style error handling ### Anti pattern code example callback style error handling
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
getData(someParameter, function(err, result) { getData(someParameter, function(err, result) {
if(err !== null) { if(err !== null) {
@ -45,7 +49,7 @@ getData(someParameter, function(err, result) {
getMoreData(b, function(c) { getMoreData(b, function(c) {
getMoreData(d, function(e) { getMoreData(d, function(e) {
if(err !== null ) { if(err !== null ) {
// you get the idea? // you get the idea?
} }
}) })
}); });
@ -54,6 +58,31 @@ getData(someParameter, function(err, result) {
} }
}); });
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
getData(someParameter, function(err: Error | null, resultA: ResultA) {
if(err !== null) {
// do something like calling the given callback function and pass the error
getMoreData(resultA, function(err: Error | null, resultB: ResultB) {
if(err !== null) {
// do something like calling the given callback function and pass the error
getMoreData(resultB, function(resultC: ResultC) {
getMoreData(resultC, function(err: Error | null, d: ResultD) {
if(err !== null) {
// you get the idea?
}
})
});
}
});
}
});
```
</details>
### Blog Quote: "We have a problem with promises" ### Blog Quote: "We have a problem with promises"

View File

@ -4,7 +4,7 @@
### One Paragraph Explainer ### One Paragraph Explainer
Typically, most of modern Node.js/Express application code runs within promises whether within the .then handler, a function callback or in a catch block. Surprisingly, unless a developer remembered to add a .catch clause, errors thrown at these places are not handled by the uncaughtException event-handler and disappear. Recent versions of Node added a warning message when an unhandled rejection pops, though this might help to notice when things go wrong but it's obviously not a proper error handling method. The straightforward solution is to never forget adding .catch clauses within each promise chain call and redirect to a centralized error handler. However, building your error handling strategy only on developers discipline is somewhat fragile. Consequently, its highly recommended using a graceful fallback and subscribe to `process.on(unhandledRejection, callback)` this will ensure that any promise error, if not handled locally, will get its treatment. Typically, most of modern Node.js/Express application code runs within promises whether within the .then handler, a function callback or in a catch block. Surprisingly, unless a developer remembered to add a .catch clause, errors thrown at these places are not handled by the uncaughtException event-handler and disappear. Recent versions of Node added a warning message when an unhandled rejection pops, though this might help to notice when things go wrong but it's obviously not a proper error handling method. The straightforward solution is to never forget adding .catch clauses within each promise chain call and redirect to a centralized error handler. However, building your error handling strategy only on developers discipline is somewhat fragile. Consequently, its highly recommended using a graceful fallback and subscribe to `process.on('unhandledRejection', callback)` this will ensure that any promise error, if not handled locally, will get its treatment.
<br/><br/> <br/><br/>
@ -13,29 +13,54 @@ Typically, most of modern Node.js/Express application code runs within promises
```javascript ```javascript
DAL.getUserById(1).then((johnSnow) => { DAL.getUserById(1).then((johnSnow) => {
// this error will just vanish // this error will just vanish
if(johnSnow.isAlive == false) if(johnSnow.isAlive === false)
throw new Error('ahhhh'); throw new Error('ahhhh');
}); });
``` ```
<br/><br/> <br/><br/>
### Code example: Catching unresolved and rejected promises ### Code example: Catching unresolved and rejected promises
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
process.on('unhandledRejection', (reason, p) => { process.on('unhandledRejection', (reason, p) => {
// I just caught an unhandled promise rejection, since we already have fallback handler for unhandled errors (see below), let throw and let him handle that // I just caught an unhandled promise rejection,
// since we already have fallback handler for unhandled errors (see below),
// let throw and let him handle that
throw reason; throw reason;
}); });
process.on('uncaughtException', (error) => { process.on('uncaughtException', (error) => {
// I just received an error that was never handled, time to handle it and then decide whether a restart is needed // I just received an error that was never handled, time to handle it and then decide whether a restart is needed
errorManagement.handler.handleError(error); errorManagement.handler.handleError(error);
if (!errorManagement.handler.isTrustedError(error)) if (!errorManagement.handler.isTrustedError(error))
process.exit(1); process.exit(1);
}); });
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
process.on('unhandledRejection', (reason: string, p: Promise<any>) => {
// I just caught an unhandled promise rejection,
// since we already have fallback handler for unhandled errors (see below),
// let throw and let him handle that
throw reason;
});
process.on('uncaughtException', (error: Error) => {
// I just received an error that was never handled, time to handle it and then decide whether a restart is needed
errorManagement.handler.handleError(error);
if (!errorManagement.handler.isTrustedError(error))
process.exit(1);
});
```
</details>
<br/><br/> <br/><br/>
@ -46,16 +71,16 @@ process.on('uncaughtException', (error) => {
> Lets test your understanding. Which of the following would you expect to print an error to the console? > Lets test your understanding. Which of the following would you expect to print an error to the console?
```javascript ```javascript
Promise.resolve(promised value).then(() => { Promise.resolve('promised value').then(() => {
throw new Error(error); throw new Error('error');
}); });
Promise.reject(error value).catch(() => { Promise.reject('error value').catch(() => {
throw new Error(error); throw new Error('error');
}); });
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
throw new Error(error); throw new Error('error');
}); });
``` ```

View File

@ -6,6 +6,9 @@ Without one dedicated object for error handling, greater are the chances of impo
### Code Example a typical error flow ### Code Example a typical error flow
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
// DAL layer, we don't handle errors here // DAL layer, we don't handle errors here
DB.addDocument(newCustomer, (error, result) => { DB.addDocument(newCustomer, (error, result) => {
@ -33,9 +36,46 @@ app.use(async (err, req, res, next) => {
} }
}); });
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
// DAL layer, we don't handle errors here
DB.addDocument(newCustomer, (error: Error, result: Result) => {
if (error)
throw new Error("Great error explanation comes here", other useful parameters)
});
// API route code, we catch both sync and async errors and forward to the middleware
try {
customerService.addNew(req.body).then((result: Result) => {
res.status(200).json(result);
}).catch((error: Error) => {
next(error)
});
}
catch (error) {
next(error);
}
// Error handling middleware, we delegate the handling to the centralized error handler
app.use(async (err: Error, req: Request, res: Response, next: NextFunction) => {
const isOperationalError = await errorHandler.handleError(err);
if (!isOperationalError) {
next(err);
}
});
```
</details>
### Code example handling errors within a dedicated object ### Code example handling errors within a dedicated object
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
module.exports.handler = new errorHandler(); module.exports.handler = new errorHandler();
@ -48,9 +88,31 @@ function errorHandler() {
}; };
} }
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
class ErrorHandler {
public async handleError(err: Error): Promise<void> {
await logger.logError(err);
await sendMailToAdminIfCritical();
await saveInOpsQueueIfCritical();
await determineIfOperationalError();
};
}
export const handler = new ErrorHandler();
```
</details>
### Code Example Anti Pattern: handling errors within the middleware ### Code Example Anti Pattern: handling errors within the middleware
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
// middleware handling the error directly, who will handle Cron jobs and testing errors? // middleware handling the error directly, who will handle Cron jobs and testing errors?
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
@ -63,6 +125,25 @@ app.use((err, req, res, next) => {
} }
}); });
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
// middleware handling the error directly, who will handle Cron jobs and testing errors?
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.logError(err);
if (err.severity == errors.high) {
mailer.sendMail(configuration.adminMail, 'Critical error occured', err);
}
if (!err.isOperational) {
next(err);
}
});
```
</details>
### Blog Quote: "Sometimes lower levels cant do anything useful except propagate the error to their caller" ### Blog Quote: "Sometimes lower levels cant do anything useful except propagate the error to their caller"

View File

@ -22,11 +22,15 @@ function addNewMember(newMember) {
Joi.assert(newMember, memberSchema); //throws if validation fails Joi.assert(newMember, memberSchema); //throws if validation fails
// other logic here // other logic here
} }
``` ```
### Anti-pattern: no validation yields nasty bugs ### Anti-pattern: no validation yields nasty bugs
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
// if the discount is positive let's then redirect the user to print his discount coupons // if the discount is positive let's then redirect the user to print his discount coupons
function redirectToPrintDiscount(httpResponse, member, discount) { function redirectToPrintDiscount(httpResponse, member, discount) {
@ -37,8 +41,24 @@ function redirectToPrintDiscount(httpResponse, member, discount) {
redirectToPrintDiscount(httpResponse, someMember); redirectToPrintDiscount(httpResponse, someMember);
// forgot to pass the parameter discount, why the heck was the user redirected to the discount screen? // forgot to pass the parameter discount, why the heck was the user redirected to the discount screen?
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
// if the discount is positive let's then redirect the user to print his discount coupons
function redirectToPrintDiscount(httpResponse: Response, member: Member, discount: number) {
if (discount != 0) {
httpResponse.redirect(`/discountPrintView/${member.id}`);
}
}
redirectToPrintDiscount(httpResponse, someMember, -12);
// We passed a negative parameter discount, why the heck was the user redirected to the discount screen?
```
</details>
### Blog Quote: "You should throw these errors immediately" ### Blog Quote: "You should throw these errors immediately"

View File

@ -6,6 +6,9 @@ Distinguishing the following two error types will minimize your app downtime and
### Code Example marking an error as operational (trusted) ### Code Example marking an error as operational (trusted)
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
// marking an error object as operational // marking an error object as operational
const myError = new Error("How can I add new product when no value provided?"); const myError = new Error("How can I add new product when no value provided?");
@ -25,6 +28,34 @@ class AppError {
throw new AppError(errorManagement.commonErrors.InvalidInput, "Describe here what happened", true); throw new AppError(errorManagement.commonErrors.InvalidInput, "Describe here what happened", true);
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
// some centralized error factory (see other examples at the bullet "Use only the built-in Error object")
export class AppError extends Error {
public readonly commonType: string;
public readonly isOperational: boolean;
constructor(commonType: string, description: string, isOperational: boolean) {
super(description);
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
this.commonType = commonType;
this.isOperational = isOperational;
Error.captureStackTrace(this);
}
}
// marking an error object as operational (true)
throw new AppError(errorManagement.commonErrors.InvalidInput, "Describe here what happened", true);
```
</details>
### Blog Quote: "Programmer errors are bugs in the program" ### Blog Quote: "Programmer errors are bugs in the program"

View File

@ -6,12 +6,15 @@ Somewhere within your code, an error handler object is responsible for deciding
### Code example: deciding whether to crash ### Code example: deciding whether to crash
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
// Assuming developers mark known operational errors with error.isOperational=true, read best practice #3 // Assuming developers mark known operational errors with error.isOperational=true, read best practice #3
process.on('uncaughtException', function(error) { process.on('uncaughtException', function(error) {
errorManagement.handler.handleError(error); errorManagement.handler.handleError(error);
if(!errorManagement.handler.isTrustedError(error)) if(!errorManagement.handler.isTrustedError(error))
process.exit(1) process.exit(1)
}); });
// centralized error handler encapsulates error-handling related logic // centralized error handler encapsulates error-handling related logic
@ -28,6 +31,51 @@ function errorHandler() {
} }
} }
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
// Assuming developers mark known operational errors with error.isOperational=true, read best practice #3
process.on('uncaughtException', (error: Error) => {
errorManagement.handler.handleError(error);
if(!errorManagement.handler.isTrustedError(error))
process.exit(1)
});
// centralized error object that derives from Nodes Error
export class AppError extends Error {
public readonly isOperational: boolean;
constructor(description: string, isOperational: boolean) {
super(description);
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
this.isOperational = isOperational;
Error.captureStackTrace(this);
}
}
// centralized error handler encapsulates error-handling related logic
class ErrorHandler {
public async handleError(err: Error): Promise<void> {
await logger.logError(err);
await sendMailToAdminIfCritical();
await saveInOpsQueueIfCritical();
await determineIfOperationalError();
};
public isTrustedError(error: Error) {
if (error instanceof AppError) {
return error.isOperational;
}
return false;
}
}
export const handler = new ErrorHandler();
```
</details>
### Blog Quote: "The best way is to crash" ### Blog Quote: "The best way is to crash"

View File

@ -6,6 +6,9 @@ Testing happy paths is no better than testing failures. Good testing code
### Code example: ensuring the right exception is thrown using Mocha & Chai ### Code example: ensuring the right exception is thrown using Mocha & Chai
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
describe("Facebook chat", () => { describe("Facebook chat", () => {
it("Notifies on new chat message", () => { it("Notifies on new chat message", () => {
@ -14,13 +17,30 @@ describe("Facebook chat", () => {
expect(chatService.sendMessage.bind({ message: "Hi" })).to.throw(ConnectionError); expect(chatService.sendMessage.bind({ message: "Hi" })).to.throw(ConnectionError);
}); });
}); });
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
describe("Facebook chat", () => {
it("Notifies on new chat message", () => {
const chatService = new chatService();
chatService.participants = getDisconnectedParticipants();
expect(chatService.sendMessage.bind({ message: "Hi" })).to.throw(ConnectionError);
});
});
```
</details>
### Code example: ensuring API returns the right HTTP error code ### Code example: ensuring API returns the right HTTP error code
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
it("Creates new Facebook group", function (done) { it("Creates new Facebook group", (done) => {
var invalidGroupInfo = {}; var invalidGroupInfo = {};
httpRequest({ httpRequest({
method: 'POST', method: 'POST',
@ -30,9 +50,33 @@ it("Creates new Facebook group", function (done) {
json: true json: true
}).then((response) => { }).then((response) => {
// if we were to execute the code in this block, no error was thrown in the operation above // if we were to execute the code in this block, no error was thrown in the operation above
}).catch(function (response) { }).catch((response) => {
expect(400).to.equal(response.statusCode); expect(400).to.equal(response.statusCode);
done(); done();
}); });
}); });
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
it("Creates new Facebook group", async () => {
let invalidGroupInfo = {};
try {
const response = await httpRequest({
method: 'POST',
uri: "facebook.com/api/groups",
resolveWithFullResponse: true,
body: invalidGroupInfo,
json: true
})
// if we were to execute the code in this block, no error was thrown in the operation above
expect.fail('The request should have failed')
} catch(response) {
expect(400).to.equal(response.statusCode);
}
});
```
</details>

View File

@ -11,7 +11,7 @@ We all love console.log but obviously, a reputable and persistent logger like [W
```javascript ```javascript
// your centralized logger object // your centralized logger object
var logger = new winston.Logger({ const logger = new winston.Logger({
level: 'info', level: 'info',
transports: [ transports: [
new (winston.transports.Console)() new (winston.transports.Console)()
@ -20,22 +20,20 @@ var logger = new winston.Logger({
// custom code somewhere using the logger // custom code somewhere using the logger
logger.log('info', 'Test Log Message with some parameter %s', 'some parameter', { anything: 'This is metadata' }); logger.log('info', 'Test Log Message with some parameter %s', 'some parameter', { anything: 'This is metadata' });
``` ```
### Code Example Querying the log folder (searching for entries) ### Code Example Querying the log folder (searching for entries)
```javascript ```javascript
var options = { const options = {
from: new Date - 24 * 60 * 60 * 1000, from: Date.now() - 24 * 60 * 60 * 1000,
until: new Date, until: new Date(),
limit: 10, limit: 10,
start: 0, start: 0,
order: 'desc', order: 'desc',
fields: ['message'] fields: ['message']
}; };
// Find items logged between today and yesterday. // Find items logged between today and yesterday.
winston.query(options, function (err, results) { winston.query(options, function (err, results) {
// execute callback with results // execute callback with results

View File

@ -38,6 +38,9 @@ if(!productToAdd)
### Code example doing it even better ### Code example doing it even better
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
// centralized error object that derives from Nodes Error // centralized error object that derives from Nodes Error
function AppError(name, httpCode, description, isOperational) { function AppError(name, httpCode, description, isOperational) {
@ -55,6 +58,38 @@ module.exports.AppError = AppError;
if(user == null) if(user == null)
throw new AppError(commonErrors.resourceNotFound, commonHTTPErrors.notFound, "further explanation", true) throw new AppError(commonErrors.resourceNotFound, commonHTTPErrors.notFound, "further explanation", true)
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
// centralized error object that derives from Nodes Error
export class AppError extends Error {
public readonly name: string;
public readonly httpCode: HttpCode;
public readonly isOperational: boolean;
constructor(name: string, httpCode: HttpCode, description: string, isOperational: boolean) {
super(description);
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
this.name = name;
this.httpCode = httpCode;
this.isOperational = isOperational;
Error.captureStackTrace(this);
}
}
// client throwing an exception
if(user == null)
throw new AppError(commonErrors.resourceNotFound, commonHTTPErrors.notFound, "further explanation", true)
```
</details>
*Explanation about the `Object.setPrototypeOf` in Typescript: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget*
### Blog Quote: "I dont see the value in having lots of different types" ### Blog Quote: "I dont see the value in having lots of different types"

View File

@ -8,39 +8,54 @@ The latest Express generator comes with a great practice that is worth to keep -
<br/><br/> <br/><br/>
### Code example: API declaration, should reside in app.js ### Code example: API declaration, should reside in app.js/app.ts
```javascript ```javascript
var app = express(); const app = express();
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use("/api/events", events.API); app.use("/api/events", events.API);
app.use("/api/forms", forms); app.use("/api/forms", forms);
``` ```
<br/><br/>
### Code example: Server network declaration, should reside in /bin/www ### Code example: Server network declaration, should reside in /bin/www
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
var app = require('../app'); var app = require('../app');
var http = require('http'); var http = require('http');
/** // Get port from environment and store in Express.
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000'); var port = normalizePort(process.env.PORT || '3000');
app.set('port', port); app.set('port', port);
/** // Create HTTP server.
* Create HTTP server.
*/
var server = http.createServer(app); var server = http.createServer(app);
``` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
import app from '../app';
import http from 'http';
// Get port from environment and store in Express.
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
// Create HTTP server.
const server = http.createServer(app);
```
</details>
### Example: test your API in-process using supertest (popular testing package) ### Example: test your API in-process using supertest (popular testing package)
<details>
<summary><strong>Javascript</strong></summary>
```javascript ```javascript
const app = express(); const app = express();
@ -56,4 +71,28 @@ request(app)
.end(function(err, res) { .end(function(err, res) {
if (err) throw err; if (err) throw err;
}); });
```` ```
</details>
<details>
<summary><strong>Typescript</strong></summary>
```typescript
const app = express();
app.get('/user', (req: Request, res: Response) => {
res.status(200).json({ name: 'tobi' });
});
request(app)
.get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '15')
.expect(200)
.end((err: Error) => {
if (err) throw err;
});
```
</details>