Optional imports

These could also be called "graceful imports" but I think "optional imports" is a nice parallel to "optional chaining".

Currently, when even a single import fails to load, the entire module containing that import fails to load, which has a domino effect on all modules importing that and so on. The result is ES apps that can be pretty fragile, especially when loading modules over HTTP: Every time one imports from a URL they risk their entire app breaking if some third party server is down or some file gets moved.
This is less of an issue for bare specifiers, but it can still happen.

Sure, one can already use dynamic import() for this:

let foo = null;

try {
  foo = await import("https://path.to/my/nonexistent-module.js");
} catch (e) {}

// foo is now null

However, this is so heavyweight that it reflects fundamentally different expectations. It's more "I expect this to fail and plan ahead" not "this is one of my dependencies and don't expect it to fail, but this is the Web and :poop: happens". Also, it's more awkward to type, and you lose the benefits of static analysis.

What about a more resilient import, where if the request fails for whatever reason, no error is thrown and all imports simply become null (or undefined?).

It could look like this:

import? foo from "https://path.to/my/nonexistent-module.js";

// foo is now null

There is precedent in other languages:

I can see lots of use cases where i'd also want a fallback in case that did fail - can you see that working in somehow?

Good point!

In my Weak imports proposal I proposed the idea of "specifier stacks" (akin to font stacks in CSS), these could be useful here too; if you agree I could post a separate proposal.

1 Like

Would you be able to expand on this? While the import is named "dynamic" it is being called with a string literal in a non-conditional way. So it is technically just as "static".

1 Like

When getting a fallback module, it could be nice to have the fallback be stored in a separate variable.

So, instead of import x from 'firstTryMe,thenTryMe';, it might be nice to have something akin to

import? x from 'firstTryMe'
else import? y from 'thenTryMe';

i.e. first you import from the first location. If that fails, import, but into a different variable, from the second location.

Maybe we could even drop the question marks, and just use the else entirely. else import means "if the previous import fails, try this one instead", and "else skip" means "if the previous import fails, just don't worry about it".


// Optional import
import x from 'optionalImport' else skip;

// Optional import with one fallback
import x from 'firstTryMe'
else import y from 'thenTryMe'
else skip;

// Optional import with one fallback. One or the other must succeed.
import x from 'firstTryMe'
else import y from 'thenTryMe';

What about this syntax?

// Required
import x from "foo" ?? "bar" ?? ...;

// Optional
import? x from "foo" ?? "bar" ?? ...;

And import fallbacks would be resolved at link time, if either the module itself or the requested exports are missing. This would allow for use at build time, not just runtime, and make fallbacks more statically analyzable (and manageable) for bundlers.

Wrapper modules could be used for more complicated fallbacks, so no alternatives are provided for that.

Leaving dynamic import out, since that's a whole can of worms itself that has better ways to handle it.

Why not utilise the existing import syntax?

import xyz from "./scriptX.js" assert { optional: true }
import xyz from "./scriptY.js" assert { optional: true }
import xyz from "./scriptZ.js" assert { optional: true }

// 'xyz' will be whichever import first resolve successfully or undefined if non of them succeed.

Or maybe

import xyz from ["./scriptX.js", "./scriptY.js", "./scriptZ.js", {}]

// the last member of the array can be an object literal or undefined or the throw keyword, if not specified undefined by default.
// If undefined then all the imported items will be undefined.