Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I'm a big fan of the new operators.

One of my remaining gripes with Javascript/Typescript is the try/catch mess around await. It makes assignment of const a pain for async calls that may reject.

e.g.

    let result: SomeType;
    try {
        result = await funcThatReturnSomeType();
    } catch (err) {
        doSomethingWithErr(err);
    }

    // at this point result is `SomeType | undefined`
    
    if (result) {
        doSomething(result);
    }
I really want some kind of structures that allow me to make `result` constant. In some cases I've rolled my own Maybe/Either wrapper and then move the try/await/catch into a function but that is still a pain.

This is such a common pattern in my code ... I wish there was a more elegant way to deal with it.



Can you hoist the `if (result)` into the `try` part of the statement? (Without seeing more context hard to know why that wouldn't work for you).

Another pattern to avoid the above is to remember that async functions return promises and that .catch() also returns a promise. So your above logic can be written as:

  const result = await funcThatReturnSomeType().catch(doSomethingWithErr);
  if (result) {
    doSomething(result);
  }


And if you hate the indentation from the `if (result) {}` you can combine this with the poor man's ? operator.

    const result = await funcThatReturnSomeType().catch(convertError); // result: SomeType | Error
    if (isError(result)) return result;
    // now result: SomeType
EDIT: the ? operator in question - https://doc.rust-lang.org/edition-guide/rust-2018/error-hand...


You can also get rid of `if(result){}` by setting the return type of "doSomethingWithErr" to "never":

    function doSomethingWithErr(err: any): never {
        throw new Error("Oops");
    }

    let result: SomeType;
    try {
        result = await funcThatReturnSomeType();
    } catch (err) {
        doSomethingWithErr(err);
    }
    // because doSomethingWithErr has return type "never", result will be definitely assigned.
    doSomething(result);

..or just return in the catch block.


Interestingly when I was encountering this myself recently, I discovered that JS finally blocks can return after a function has nominally already returned. Consider the following closure.

    (() => {
      try {
        // finally will return prior to this console.log
        console.log('this try was executed and the return ignored')
        return 'try block'
      } catch (e) {
        return 'error block'
      } finally {
        return 'finally block'
      }  
    })()


I don't think that's the right way of thinking about it. The behavior I see is consistent with my understanding of `finally` from other languages.

Basically, `finally` gives you a guarantee that it will actually run once the `try` block is exited. Likewise, `return` effectively assigns the return value and exits. But it doesn't (cannot and should not) breach the contract of try-finally, since the purpose of try-finally is to ensure resources are managed correctly, so it either exits to the caller or it exits to the finally block, depending on which one is most recent.

In your case, a return value is assigned and the `try` block is exited using `return`. We then have to continue into the `finally` block, since that is the core meaning of `finally` - we run it after we leave `try`. And then with `return`, we reassign the return value and finally leave the whole function. At this point, the return value is the second one that was assigned to it.

Maybe thinking of it like this is helpful, although I somewhat hope it isn't. You can see that "return" is reassigned before we have a chance to read it. I've simplified by removing any consideration of errors, but I console.logged the final output.

    //this is the function call at the end of your IIFP
    next_code.push(AfterMe)
    goto Anonymous

    // this is the function definition
    Anonymous:
        // this is the try-finally idiom
        next_code.push(FinallyBlock);
        //this is the try
        console.log("this try was executed");
        //these two lines are the first return
        var return = 'try block';
        goto next_code.pop();
        //this is the finally
        FinallyBlock:
            var return = 'finally block'
            goto next_code.pop();

    // this code gets executed from the FinallyBlock's goto and is as if you have a console.log(..) around your whole definition.
    AfterMe:
        console.log(result)


It's probably either this behavior or a statement in the finally block that doesn't run, even though it's guaranteed to. Either way, some assumption of normal program behavior is invalidated.

Unless one goes the PowerShell way and just forbids returning from finally.


This is how it's specified for anyone interested: https://stackoverflow.com/a/3838130/298073


What the hell?


> Can you hoist the `if (result)` into the `try` part of the statement?

And now you wrap the function call to doSomething() in the try/catch too. Often (usually?) the try/catch specifically is for the asynchronous function. Usually that's because the async. stuff might fail due to expectable (even if undesirable) runtime conditions (e.g. "file not found"), while synchronous code should work for the most part (external condition errors vs. coding errors - and your catch is about the former, because, for example, you might want coding errors to just crash the app and be caught during testing).

Sure, you can claim that you check for specific errors that could only happen in that function, so that any errors occurring in doSomething() don't matter/don't change the outcome, or that doSomething never throws because you are sure of the code (the async. function may throw based on runtime conditions, but you may have development-time control over any issues in doSomethign() - but if you start going down that path, having to rely on the developer doing there job perfectly for each such construct, later maintainability and readability goes down the drain. You would have to make sure such a claim is valid when you later come across the construct. That is why you really don't want anything inside the try/catch from which you don't want to see any errors in the catch block. So my policy is to never ever do that even if in the given context it would work - it places additional work on whoever is going to read that section later (or they are ignorant of the problem and won't see this potential problem, which is not any better).


I’ve run into the exact same situation and continue to repeatedly. In fact this is a stupidly common pattern in a library in working on at work right now.

My solutions have involved casts (and comments explaining the assumptions involved) instead of the ‘if’ statement more times than I’d like to have done, but it ends up with the same result with the added (however small) compute with the conditional since it can be safe to assume the value is not undefined. It’s not perfect, but it at least omits unnecessary runtime code.


Its unnecessary until the clause above it changes and the assumption no longer holds. That's how I fight my urge to go with the cast.


That’s totally fair. Admittedly in my situation I have high degree of control over the input so my constraints are probably a little looser.

At any rate, I’d appreciate the same thing you would in these cases.


I use await-to-js[1] to clean this stuff up. You can pair it with destructuring arrays like so:

const [err, result] = await to(funcThatReturnSomeType());

if(err) doSomethingWithErr(err);

doSomething(result);

[1] https://www.npmjs.com/package/await-to-js


This right here is exactly why Errors should be enums, instead of this wacky exception handling system. Try/catch is why I frequently prefer plain old .then and .catch instead


Good old Go-style


Yep. Doesn't even feel so different from using error first callbacks. I had to go back to using them for something recently, and feel like most of my problems back then stemmed from excessive deep nesting, and overreliance on the closure scope. Don't get me wrong I love the Promise api, but if ts were popular pre-Promises, we wouldn't have had half the issues with callbacks for concurrency. You'd be able to type functions that take callbacks, know what errors they can receive, see if anything's out of scope or misused, etc


IIFEs are an option:

    const result = await (() => {
      try {
        return funcThatReturnSomeType();
      } catch (err) {
        doSomethingWithErr(err);
      }
    })();


I don't think this is correct, as the try catch will only catch errors that happen while returning the promise, not awaiting it; you need to do this:

    const result = await (async () => {
      try {
        return await funcThatReturnSomeType();
      } catch (err) {
        doSomethingWithErr(err);
      }
    })();


You may be right, proving the old adage that the best way to get an answer on the internet is to post a wrong answer ;-)


Iffys are a nice go-to in ts in general, because type inference is so handy and I'd rather not have a big pile of typed `let`s at the top that I have to assign into later. Even something as simple as a switch plays nicer if you iffy it and use it like an expression (similar to a match is rust)


That is so much harder to read though...


Could do something like this:

    async function test() {
        const promise = Math.random() < 0.5 ?
                        Promise.resolve('wow') :
                        Promise.reject('oops');
        try {
            await promise;
        } catch (e) {
            console.log('caught');
            return;
        }
        const result = await promise;
        console.log('result', result);
    }
    test();


Another commenter pointed out that .catch is perfectly fine in the async/await world, even though you pretty much never use .then anymore. Here's another way of doing it:

  try {
    doSomething(await funcThatReturnSomeType());
  } catch (err) {
    doSomethingWithErr(err);
  }


They're not equivalent if doSomething might throw and you don't want to catch it.


Right, which goes back to the question shtylman asked about why doSomething couldn't be part of the try block. I'm tossing this form out there in case it helps. People sometimes forget that await can go wherever an expression goes.


You are unnecessarily wrapping it into Maybe. Promise.then/.catch is essentially what you want.


There's this keyword called "var"


`let` and `const` are transpiled by Typescript/babel to `var`.


The point is you can put var inside the try because it has function scope. That's what I do, although it is inelegant.


Only if the target is ES3 or ES5.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: