Skip to content

Commit 1d8268f

Browse files
committed
feat(fullStack): add ability to get reducer/middleware creation stack traces in dev mode
1 parent c8d8a49 commit 1d8268f

File tree

10 files changed

+179
-14
lines changed

10 files changed

+179
-14
lines changed

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,54 @@ setConfigEntry('hello', 'world').meta // {key: 'hello', domain: 'config'}
143143
forConfigDomain(setEntry('hello', 'world')).meta // {key: 'hello', domain: 'config'}
144144
```
145145

146+
## fullStack(error: Error, wrapped?: (error: Error) => string): string
147+
148+
Errors thrown from the sub-reducers you pass to `createReducer`, `composeReducers`, 'prefixReducer', or sub-middleware
149+
you pass to `createMiddleware` or `composeMiddleware` normally don't include any information about where the associated
150+
call to `createReducer` etc. occurred, making debugging difficult. However, in dev mode, `mindfront-redux-utils` adds
151+
this info to the resulting reducers and middleware, and you can get it by calling `fullStack`, like so:
152+
153+
```js
154+
import {createReducer, fullStack} from './src'
155+
156+
function hello() {
157+
throw new Error("TEST")
158+
}
159+
const r = createReducer({hello})
160+
161+
try {
162+
r({}, {type: 'hello'})
163+
} catch (e) {
164+
console.error(fullStack(e))
165+
}
166+
```
167+
168+
Output:
169+
```
170+
Error: TEST
171+
at hello (/Users/andy/redux-utils/temp.js:4:9)
172+
at result (/Users/andy/redux-utils/src/createReducer.js:19:24)
173+
at withCause (/Users/andy/redux-utils/src/addCreationStack.js:5:14)
174+
at Object.<anonymous> (/Users/andy/redux-utils/temp.js:9:3)
175+
at Module._compile (module.js:556:32)
176+
at loader (/Users/andy/redux-utils/node_modules/babel-register/lib/node.js:144:5)
177+
at Object.require.extensions.(anonymous function) [as .js] (/Users/andy/redux-utils/node_modules/babel-register/lib/node.js:154:7)
178+
at Module.load (module.js:473:32)
179+
at tryModuleLoad (module.js:432:12)
180+
at Function.Module._load (module.js:424:3)
181+
Caused by reducer created at:
182+
at addCreationStack (/Users/andy/redux-utils/src/addCreationStack.js:2:21)
183+
at createReducer (/Users/andy/redux-utils/src/createReducer.js:25:55)
184+
at Object.<anonymous> (/Users/andy/redux-utils/temp.js:6:11)
185+
at Module._compile (module.js:556:32)
186+
at loader (/Users/andy/redux-utils/node_modules/babel-register/lib/node.js:144:5)
187+
at Object.require.extensions.(anonymous function) [as .js] (/Users/andy/redux-utils/node_modules/babel-register/lib/node.js:154:7)
188+
at Module.load (module.js:473:32)
189+
at tryModuleLoad (module.js:432:12)
190+
at Function.Module._load (module.js:424:3)
191+
at Function.Module.runMain (module.js:590:10)
192+
```
193+
194+
If you are using [VError](https://github.com/joyent/node-verror), you may pass VError's `fullStack` function as the
195+
second argument to also include the cause chain from `VError`.
146196

src/addCreationStack.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default function addCreationStack(fn, what) {
2+
const createdAt = new Error(what + ' created at:')
3+
return function withCause(...args) {
4+
try {
5+
return fn(...args)
6+
} catch (error) {
7+
if (!error.creationStack) error.creationStack = () => createdAt.stack
8+
throw error
9+
}
10+
}
11+
}
12+

src/composeMiddleware.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import forEach from 'lodash.foreach'
33
import mapValues from 'lodash.mapvalues'
44
import createMiddleware from './createMiddleware'
55
import checkForNonFunctions from './checkForNonFunctions'
6+
import addCreationStack from './addCreationStack'
67

78
export default function composeMiddleware(...middlewares) {
9+
if (process.env.NODE_ENV !== 'production') checkForNonFunctions(middlewares, 'middlewares')
10+
811
if (middlewares.length === 0) return store => dispatch => dispatch
912
if (middlewares.length === 1) return middlewares[0]
1013

11-
if (process.env.NODE_ENV !== 'production') checkForNonFunctions(middlewares, 'middlewares')
12-
1314
if (every(middlewares, middleware => middleware.actionHandlers)) {
1415
// regroup all the action handlers in the middlewares by action type.
1516
let actionHandlers = {}
@@ -20,5 +21,10 @@ export default function composeMiddleware(...middlewares) {
2021
})
2122
return createMiddleware(mapValues(actionHandlers, typeHandlers => composeMiddleware(...typeHandlers)))
2223
}
23-
return store => next => middlewares.reduceRight((next, handler) => handler(store)(next), next)
24+
25+
return store => next => {
26+
let handleAction = middlewares.reduceRight((next, handler) => handler(store)(next), next)
27+
if (process.env.NODE_ENV !== 'production') handleAction = addCreationStack(handleAction, 'middleware')
28+
return handleAction
29+
}
2430
}

src/composeReducers.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import forEach from 'lodash.foreach'
44
import mapValues from 'lodash.mapvalues'
55
import createReducer from './createReducer'
66
import checkForNonFunctions from './checkForNonFunctions'
7+
import addCreationStack from './addCreationStack'
78

89
export default function composeReducers(...reducers) {
9-
if (reducers.length === 0) return state => state
10-
if (reducers.length === 1) return reducers[0]
11-
1210
if (process.env.NODE_ENV !== 'production') checkForNonFunctions(reducers, 'reducers')
1311

12+
let result
13+
if (reducers.length === 0) result = state => state
14+
if (reducers.length === 1) result = reducers[0]
15+
1416
// if all reducers have actionHandlers maps, merge the maps using composeReducers
15-
if (every(reducers, reducer => reducer.actionHandlers instanceof Object)) {
17+
else if (every(reducers, reducer => reducer.actionHandlers instanceof Object)) {
1618
let actionHandlers = {}
1719
let initialState
1820
reducers.forEach(reducer => {
@@ -23,7 +25,10 @@ export default function composeReducers(...reducers) {
2325
})
2426
return createReducer(initialState, mapValues(actionHandlers,
2527
typeHandlers => composeReducers(...typeHandlers)))
28+
} else {
29+
result = (state, action) => reduce(reducers, (state, reducer) => reducer(state, action), state)
2630
}
31+
if (process.env.NODE_ENV !== 'production') result = addCreationStack(result, 'reducer')
2732

28-
return (state, action) => reduce(reducers, (state, reducer) => reducer(state, action), state)
33+
return result
2934
}

src/createMiddleware.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import checkForNonFunctions from './checkForNonFunctions'
2+
import addCreationStack from './addCreationStack'
23

34
export default function createMiddleware(actionHandlers) {
45
if (process.env.NODE_ENV !== 'production') checkForNonFunctions(actionHandlers, 'actionHandlers')
56

6-
const result = store => next => action => {
7-
const handler = actionHandlers[action.type]
8-
if (!handler) return next(action)
9-
return handler(store)(next)(action)
7+
let result = store => next => {
8+
let handleAction = action => {
9+
const handler = actionHandlers[action.type]
10+
if (!handler) return next(action)
11+
return handler(store)(next)(action)
12+
}
13+
if (process.env.NODE_ENV !== 'production') handleAction = addCreationStack(handleAction, 'middleware')
14+
return handleAction
1015
}
1116
result.actionHandlers = actionHandlers
1217
return result

src/createReducer.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import size from 'lodash.size'
22
import checkForNonFunctions from './checkForNonFunctions'
3+
import addCreationStack from './addCreationStack'
34

45
export default function createReducer(initialState, actionHandlers) {
56
if (arguments.length === 1) {
67
actionHandlers = initialState
78
initialState = undefined
89
}
910

10-
if (process.env.NODE_ENV !== 'production') checkForNonFunctions(actionHandlers, 'actionHandlers')
11+
if (process.env.NODE_ENV !== 'production') {
12+
checkForNonFunctions(actionHandlers, 'actionHandlers')
13+
}
1114

1215
let result
1316
if (size(actionHandlers)) {
@@ -19,6 +22,7 @@ export default function createReducer(initialState, actionHandlers) {
1922
else {
2023
result = (state = initialState) => state
2124
}
25+
if (process.env.NODE_ENV !== 'production') result = addCreationStack(result, 'reducer')
2226
result.initialState = initialState
2327
result.actionHandlers = actionHandlers
2428
return result

src/fullStack.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function fullStack(error, wrapped = error => error.stack) {
2+
let result = wrapped(error)
3+
if (error.creationStack) result += '\nCaused by ' + error.creationStack().substring('Error: '.length)
4+
return result
5+
}
6+

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import createPluggableMiddleware from './createPluggableMiddleware'
88
import prefixReducer from './prefixReducer'
99
import prefixActionCreator from './prefixActionCreator'
1010
import addMeta from './addMeta'
11+
import fullStack from './fullStack'
1112

1213
export {
1314
createReducer,
@@ -18,5 +19,6 @@ export {
1819
prefixReducer,
1920
prefixActionCreator,
2021
addMeta,
22+
fullStack,
2123
}
2224

src/prefixReducer.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import mapKeys from 'lodash.mapkeys'
22
import createReducer from './createReducer'
3+
import addCreationStack from './addCreationStack'
34

45
export default function prefixReducer(prefix) {
56
return reducer => {
67
if (reducer.actionHandlers instanceof Object) {
78
return createReducer(reducer.initialState, mapKeys(reducer.actionHandlers, (handler, key) => prefix + key))
89
}
9-
return (state, action) => typeof action.type === 'string' && action.type.startsWith(prefix)
10+
let result = (state, action) => typeof action.type === 'string' && action.type.startsWith(prefix)
1011
? reducer(state, {...action, type: action.type.substring(prefix.length)})
1112
: state
13+
14+
if (process.env.NODE_ENV !== 'production') result = addCreationStack(result, 'reducer')
15+
return result
1216
}
1317
}
1418

test/creationStackTest.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {assert, expect} from 'chai'
2+
import {createReducer, composeReducers, createMiddleware, composeMiddleware, prefixReducer, fullStack} from '../src'
3+
4+
describe('addCreationStack', () => {
5+
let origNodeEnv
6+
before(() => {
7+
origNodeEnv = process.env.NODE_ENV
8+
process.env.NODE_ENV = ''
9+
})
10+
after(() => process.env.NODE_ENV = origNodeEnv)
11+
12+
describe('createReducer', () => {
13+
it('adds creation stack to errors', () => {
14+
const r = createReducer({hello: () => { throw new Error('test') }})
15+
try {
16+
r({}, {type: 'hello'})
17+
assert.fail('expected error to be thrown')
18+
} catch (error) {
19+
expect(fullStack(error)).to.match(/caused by reducer created at/i)
20+
}
21+
})
22+
})
23+
describe('composeReducers', () => {
24+
it('adds creation stack to errors', () => {
25+
const r = composeReducers(() => { throw new Error('test') })
26+
try {
27+
r({}, {type: 'hello'})
28+
assert.fail('expected error to be thrown')
29+
} catch (error) {
30+
expect(fullStack(error)).to.match(/caused by reducer created at/i)
31+
}
32+
})
33+
})
34+
describe('createMiddleware', () => {
35+
it('adds creation stack to errors', () => {
36+
const r = createMiddleware({hello: store => next => action => { throw new Error('test') }})
37+
try {
38+
r(null)(null)({type: 'hello'})
39+
assert.fail('expected error to be thrown')
40+
} catch (error) {
41+
expect(fullStack(error)).to.match(/caused by middleware created at/i)
42+
}
43+
})
44+
})
45+
describe('composeMiddleware', () => {
46+
it('adds creation stack to errors', () => {
47+
const r = composeMiddleware(
48+
store => dispatch => dispatch,
49+
store => next => action => { throw new Error('test') }
50+
)
51+
try {
52+
r(null)(null)({type: 'hello'})
53+
assert.fail('expected error to be thrown')
54+
} catch (error) {
55+
expect(fullStack(error)).to.match(/caused by middleware created at/i)
56+
}
57+
})
58+
})
59+
describe('prefixReducer', () => {
60+
it('adds creation stack to errors', () => {
61+
const r = prefixReducer('hello')(() => { throw new Error('test') })
62+
try {
63+
r({}, {type: 'hello'})
64+
assert.fail('expected error to be thrown')
65+
} catch (error) {
66+
expect(fullStack(error)).to.match(/caused by reducer created at/i)
67+
}
68+
})
69+
})
70+
})
71+

0 commit comments

Comments
 (0)