When designing asynchronous APIs that could error in Flow, prefer using .then
for both successful and failure cases. Flow exposes a relatively unsafe library definition for the .catch
method, so it’s best to avoid it if you can.
Problem
What does this look like in practice? Say you’re thinking about writing code that looks similar to this:
This is okay code, but not great. Why? Because Flow won’t prevent us from calling reject(...)
with something that’s not of type ErrResult
, and it won’t warn us when we try to use err
incorrectly. Concretely, if we had this type definition:
= string; type ErrResult
Flow wouldn’t prevent us from doing this:
// number, not a string!
reject(42);
nor from doing this:
// boolean, not a string!
.catch((err: boolean) => ...);
Solution
As mentioned, we can work around this by only using resolve
and .then
. For example, we can replace our code above with this:
There’s a lot of benefits in this newer code:
Using
resolve
is much safer thanreject
. Flow will always warn us if we callresolve
with an improperly-typed input.Using
.then
is the same. Flow will warn for improper usage, and even correctly infer the type ofres
in our handler.We got exhaustiveness as a bonus. We now handle all errors, whereas before it was easy to forget to include a
.catch
.
Caveats
Of course, there are some times when the you’re interfacing with code not under your control that exposes critical functionality over .catch
. In these cases, it’s not an option to just “not use .catch
”. Instead, you have two options.
If you trust that the library you’re using will never “misbehave”, you can ascribe a narrow type to the .catch
callback function:
// If I know that throwNumber will always call `reject` with a
// number, I can stop the loose types from propagating further
// with an explicit annotation:
throwNumber.then(() => console.log("Didn't throw!"))
// ┌── explicit annotation
.catch((n: number) => handleWhenRejected(n))
If you aren’t comfortable putting this much trust in the API, you should instead ascribe mixed
to the result of the callback.
throwNumber.then(() => console.log("Didn't throw!"))
// ┌── defensive annotation
.catch((n: mixed) => {
if (typeof n === 'number') {
handleWhenRejected(n);
else {
} // Reports misbehavior to an imaginary observability service
.increment('throwNumber.unexpected_input');
tracker
}; })