microsoft / tsyringe

Lightweight dependency injection container for JavaScript/TypeScript
MIT License
5.19k stars 172 forks source link

Original error's context is lost when replaced with "Cannot inject the dependency". #177

Open timoxley opened 3 years ago

timoxley commented 3 years ago

The tsyringe error "Cannot inject the dependency" handler is great while you're trying to iron out issues with the DI, but a consumer doesn't care/shouldn't know anything about tsyringe, nor should tsyringe be silently throwing away useful context from the original error. The way tsyringe forcibly wraps errors circumvents constructors from being able to throw more specific errors and prevents callers from responding to those specific errors, instead making every error into a vanilla Error that mostly talks about tsyringe.

See line 522:

https://github.com/microsoft/tsyringe/blob/0cb911b799ccd0b3079629865f1a8fb04cc49658/src/dependency-container.ts#L495-L524

The tsyringe error does grab the original error's .message string as the "Reason: ", but critically it loses all other context from the original error including the original error's stack, constructor and any additional properties that may have been set on it to aid debugging or control flow e.g. RangeError vs SyntaxError or error.code or err.syscall.

Ideally there would be some way to prevent tsyringe from wrapping errors, or perhaps it could attach the original error to the wrapped error so the caller can optionally ignore the tsyringe error and rethrow the original error.

I'd be happy to even find an ugly hack that works around this but I don't currently see a path to even hack it to behave better with monkeypatching etc.

timoxley commented 3 years ago

Ok, I found an ugly workaround that just rewrites the error.message property instead of creating a new Error.

One downside is that if the error's .stack getter has already been accessed, then the updated .message won't appear in the error's toString, and sometimes .message can be frozen, but IMO still better to throw the original error.

// horrible, hacky workaround to prevent tsyringe from replacing useful,
// specific errors produced by constructors with tsyringe-specific errors that
// look like "Cannot inject the dependency".  These errors lose the original
// error's stack, constructor and any additional context that was attached for
// control flow or debugging purposes e.g. err.code or err.syscall.
//
// See: https://github.com/microsoft/tsyringe/issues/177
// @ts-nocheck
import { container } from 'tsyringe'
// eslint-disable-next-line import/no-unresolved
import { isTokenDescriptor, isTransformDescriptor } from 'tsyringe/dist/cjs/providers/injection-token'
import { formatErrorCtor } from 'tsyringe/dist/cjs/error-helpers'

// Should be identical to original resolveParams, but replaces new Error with err.message = formatErrorCtor
// See: https://github.com/microsoft/tsyringe/blob/0cb911b799ccd0b3079629865f1a8fb04cc49658/src/dependency-container.ts#L495-L525
container.constructor.prototype.resolveParams = function resolveParams(context, ctor) {
    return (param, idx) => {
        try {
            if (isTokenDescriptor(param)) {
                if (isTransformDescriptor(param)) {
                    return param.multiple
                        ? this.resolve(param.transform).transform(
                            this.resolveAll(param.token),
                            ...param.transformArgs
                        )
                        : this.resolve(param.transform).transform(
                            this.resolve(param.token, context),
                            ...param.transformArgs
                        )
                // eslint-disable-next-line no-else-return
                } else {
                    return param.multiple
                        ? this.resolveAll(param.token)
                        : this.resolve(param.token, context)
                }
            // eslint-disable-next-line no-else-return
            } else if (isTransformDescriptor(param)) {
                return this.resolve(param.transform, context).transform(
                    this.resolve(param.token, context),
                    ...param.transformArgs
                )
            }
            return this.resolve(param, context)
        } catch (e) {
            e.message = formatErrorCtor(ctor, idx, e)
            throw e
        }
    }
}