Since null and undefined are their own types in TypeScript, corresponding errors are much more uncommon now. That is, until one forgets try / catch because it might be obvious if a function just returns undefined, but one can't know if it throws. And when it does, errors are often very cryptic.
Of course one could try some GO style error handling with return new Error() and if(result instanceof Error), but I think most of us would agree that it is verbose and mostly unnecessary, not to mention incompatible with existing libraries.
More often than not one just wants to know if a sequence of instructions is successful as a whole or not, so one doesn't have to check for errors after each function call. That's what try / catch can do in a convenient way.
My suggestion would be that throw emits a generic throwable<T>, which will then be catched with an inferred type. Every function that throws needs to be called inside a try / catch block if not explicitly stated otherwise, e.g. with a simple function annotation doesNotThrow()!. The same concept was introduced for variables that we know aren't null or undefined: const notNull = couldBeNull!
throw should be handled like return with throwable<T> as return type with the exception that it can only be satisfied by try / catch. That's why it shouldn't be compatible with any.
Outside a function throw should only be allowed inside a try block to avoid unhandled exceptions.
As an example: Let's assume there are two functions which return a union type with an throable:
function throwError(x: number): number | throwable<Error> {
if (x > 5) {
return x;
}
throw new Error("x is too small")
}
function throwString(x: any): any | throwable<string> {
if(x){
return x
}
throw "x is null or undefined"
}
The compiler knows that there's a chance that each function terminates with throwing an exception and it should be able to infer the type of the chatched exception (accumulated from all possible exceptions inside try).
The type throwable could only be satisfied by an try / catch block which takes an arbitrary amount of throwables, so that type isn't actually given to a receiving variable.
try {
const n: number = throwError(5);
const u: any = throwString(5)
throw new MyErrorClass("This error will be catched.")
} catch (error: Error | string | MyErrorClass) {
}
Outside a function, this should be a compiler error, because of unhandled exceptions:
throwError(5);
throwString(5)
throw "This should be an error."
The common argument against such a feature is "productivity", but I couldn't think of a situation where that could be true. Moreover, like with every other kind of check it could be easily bypassed with an annotation or disabled with a compiler flag.
Even if one doesn't care for the exact nature of an exception, which I think would be the common case, it would help a great deal to at least catch all of them.
Duplicate of https://github.com/Microsoft/TypeScript/issues/13219
type Either<L, R> = Left<T> | Right<R>;
class Left<L> { constructor (public readonly left: L, public readonly isRight = false){} }
class Right<R> { constructor (public readonly right: R, public readonly isRight = true){} }
function throwError(x: number): Either<Error, number> {
if (x > 5) {
return new Right(x);
}
return new Left(new Error("x is too small"))
}
@aleksey-bykov
That doesn't only look cumbersome, but it still leads to GO style error handling.
JavaScript's solution for error handling is perfectly fine, it just needs to be "embraced" by TypeScript to make it more approachable.
ha ha, ok, keep me posted
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.
Most helpful comment