diff --git a/docs/changelog/130158.yaml b/docs/changelog/130158.yaml
new file mode 100644
index 0000000000000..ffacbcfc740ef
--- /dev/null
+++ b/docs/changelog/130158.yaml
@@ -0,0 +1,5 @@
+pr: 130158
+summary: Handle unavailable MD5 in ES|QL
+area: ES|QL
+type: bug
+issues: []
diff --git a/server/src/main/java/org/elasticsearch/common/util/Result.java b/server/src/main/java/org/elasticsearch/common/util/Result.java
new file mode 100644
index 0000000000000..57bcb56861da9
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/common/util/Result.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.common.util;
+
+import org.elasticsearch.common.CheckedSupplier;
+
+import java.util.Optional;
+
+/**
+ * A wrapper around either
+ * 
+ * - a successful result of parameterized type {@code V}+ *
- a failure with exception type {@code E}+ *
+ */
+public abstract class Result implements CheckedSupplier {
+
+    public static  Result of(V value) {
+        return new Success<>(value);
+    }
+
+    public static  Result failure(E exception) {
+        return new Failure<>(exception);
+    }
+
+    private Result() {}
+
+    public abstract V get() throws E;
+
+    public abstract Optional failure();
+
+    public abstract boolean isSuccessful();
+
+    public boolean isFailure() {
+        return isSuccessful() == false;
+    };
+
+    public abstract Optional asOptional();
+
+    private static class Success extends Result {
+        private final V value;
+
+        Success(V value) {
+            this.value = value;
+        }
+
+        @Override
+        public V get() throws E {
+            return value;
+        }
+
+        @Override
+        public Optional failure() {
+            return Optional.empty();
+        }
+
+        @Override
+        public boolean isSuccessful() {
+            return true;
+        }
+
+        @Override
+        public Optional asOptional() {
+            return Optional.of(value);
+        }
+    }
+
+    private static class Failure extends Result {
+        private final E exception;
+
+        Failure(E exception) {
+            this.exception = exception;
+        }
+
+        @Override
+        public V get() throws E {
+            throw exception;
+        }
+
+        @Override
+        public Optional failure() {
+            return Optional.of(exception);
+        }
+
+        @Override
+        public boolean isSuccessful() {
+            return false;
+        }
+
+        @Override
+        public Optional asOptional() {
+            return Optional.empty();
+        }
+    }
+}
diff --git a/server/src/test/java/org/elasticsearch/common/util/ResultTests.java b/server/src/test/java/org/elasticsearch/common/util/ResultTests.java
new file mode 100644
index 0000000000000..cfb489b6224c6
--- /dev/null
+++ b/server/src/test/java/org/elasticsearch/common/util/ResultTests.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.common.util;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.ElasticsearchStatusException;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.test.ESTestCase;
+
+import static org.elasticsearch.test.hamcrest.OptionalMatchers.isEmpty;
+import static org.elasticsearch.test.hamcrest.OptionalMatchers.isPresentWith;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+
+public class ResultTests extends ESTestCase {
+
+    public void testSuccess() {
+        final String str = randomAlphaOfLengthBetween(3, 8);
+        final Result result = Result.of(str);
+        assertThat(result.isSuccessful(), is(true));
+        assertThat(result.isFailure(), is(false));
+        assertThat(result.get(), sameInstance(str));
+        assertThat(result.failure(), isEmpty());
+        assertThat(result.asOptional(), isPresentWith(str));
+    }
+
+    public void testFailure() {
+        final ElasticsearchException exception = new ElasticsearchStatusException(
+            randomAlphaOfLengthBetween(10, 30),
+            RestStatus.INTERNAL_SERVER_ERROR
+        );
+        final Result result = Result.failure(exception);
+        assertThat(result.isSuccessful(), is(false));
+        assertThat(result.isFailure(), is(true));
+        assertThat(expectThrows(Exception.class, result::get), sameInstance(exception));
+        assertThat(result.failure(), isPresentWith(sameInstance(exception)));
+        assertThat(result.asOptional(), isEmpty());
+    }
+
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java
index be0a7b2fe27b2..c95e229b04419 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java
@@ -11,6 +11,7 @@
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.util.Result;
 import org.elasticsearch.compute.ann.Evaluator;
 import org.elasticsearch.compute.ann.Fixed;
 import org.elasticsearch.compute.operator.BreakingBytesRefBuilder;
@@ -202,6 +203,14 @@ public static HashFunction create(BytesRef literal) throws NoSuchAlgorithmExcept
             return new HashFunction(algorithm, MessageDigest.getInstance(algorithm));
         }
 
+        public static Result tryCreate(String algorithm) {
+            try {
+                return Result.of(new HashFunction(algorithm, MessageDigest.getInstance(algorithm)));
+            } catch (NoSuchAlgorithmException e) {
+                return Result.failure(e);
+            }
+        }
+
         public HashFunction copy() {
             try {
                 return new HashFunction(algorithm, MessageDigest.getInstance(algorithm));
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Md5.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Md5.java
index b42ec1036cb5b..4d30f6b1b37f4 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Md5.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Md5.java
@@ -9,6 +9,8 @@
 
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.util.Result;
+import org.elasticsearch.xpack.esql.VerificationException;
 import org.elasticsearch.xpack.esql.core.expression.Expression;
 import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
 import org.elasticsearch.xpack.esql.core.tree.Source;
@@ -18,17 +20,24 @@
 import org.elasticsearch.xpack.esql.expression.function.scalar.string.Hash.HashFunction;
 
 import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.List;
 
 public class Md5 extends AbstractHashFunction {
 
     public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "MD5", Md5::new);
 
-    private static final HashFunction MD5 = HashFunction.create("MD5");
+    /**
+     * As of Java 14, it is permissible for a JRE to ship without the {@code MD5} {@link MessageDigest}.
+     * We want the "md5" function in ES|QL to fail at runtime on such platforms (rather than at startup)
+     * so we wrap the {@link HashFunction} in a {@link Result}.
+     */
+    private static final Result MD5 = HashFunction.tryCreate("MD5");
 
     @FunctionInfo(
         returnType = "keyword",
-        description = "Computes the MD5 hash of the input.",
+        description = "Computes the MD5 hash of the input (if the MD5 hash is available on the JVM).",
         examples = { @Example(file = "hash", tag = "md5") }
     )
     public Md5(Source source, @Param(name = "input", type = { "keyword", "text" }, description = "Input to hash.") Expression input) {
@@ -41,7 +50,12 @@ private Md5(StreamInput in) throws IOException {
 
     @Override
     protected HashFunction getHashFunction() {
-        return MD5;
+        try {
+            return MD5.get();
+        } catch (NoSuchAlgorithmException e) {
+            // Throw a new exception so that the stack trace reflects this call (rather than the static initializer for the MD5 field)
+            throw new VerificationException("function 'md5' is not available on this platform: {}", e.getMessage());
+        }
     }
 
     @Override
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java
index 871bec7c06804..61577ee56777e 100644
--- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java
@@ -13,22 +13,30 @@
 import org.elasticsearch.common.util.BigArrays;
 import org.elasticsearch.common.util.MockBigArrays;
 import org.elasticsearch.common.util.PageCacheRecycler;
+import org.elasticsearch.common.util.Result;
 import org.elasticsearch.compute.data.BlockFactory;
 import org.elasticsearch.compute.operator.DriverContext;
 import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.hamcrest.OptionalMatchers;
 import org.elasticsearch.xpack.esql.core.InvalidArgumentException;
 import org.elasticsearch.xpack.esql.core.expression.Literal;
 import org.elasticsearch.xpack.esql.core.tree.Source;
 import org.elasticsearch.xpack.esql.core.type.DataType;
 import org.junit.After;
 
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.Security;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
 import static org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase.evaluator;
 import static org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase.field;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.startsWith;
 
 public class HashStaticTests extends ESTestCase {
@@ -45,6 +53,27 @@ public void testInvalidAlgorithmLiteral() {
         assertThat(e.getMessage(), startsWith("invalid algorithm for [hast(\"invalid\", input)]: invalid MessageDigest not available"));
     }
 
+    public void testTryCreateUnavailableMd5() throws NoSuchAlgorithmException {
+        assumeFalse("We run with different security providers in FIPS, and changing them at runtime is more complicated", inFipsJvm());
+        final Provider sunProvider = Security.getProvider("SUN");
+        try {
+            Security.removeProvider("SUN");
+            final Result result = Hash.HashFunction.tryCreate("MD5");
+            assertThat(result.isSuccessful(), is(false));
+            assertThat(result.failure(), OptionalMatchers.isPresentWith(throwableWithMessage(containsString("MD5"))));
+            expectThrows(NoSuchAlgorithmException.class, result::get);
+        } finally {
+            Security.addProvider(sunProvider);
+        }
+
+        {
+            final Result result = Hash.HashFunction.tryCreate("MD5");
+            assertThat(result.isSuccessful(), is(true));
+            assertThat(result.failure(), OptionalMatchers.isEmpty());
+            assertThat(result.get().algorithm(), is("MD5"));
+        }
+    }
+
     /**
      * The following fields and methods were borrowed from AbstractScalarFunctionTestCase
      */